From d7bb0d5f04f0b5db01142f6a508c8c1293c107c6 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 19 May 2022 15:29:17 +0200 Subject: [PATCH 001/208] add new changelog section --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 044c107..ae5ac76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.x.x] - Unreleased + ## [0.2.0] - 2022/05/19 ### Added From 9c647c0252569878bfbefee39a82d4a795c0ae15 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 19 May 2022 15:32:48 +0200 Subject: [PATCH 002/208] make dev version --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 0ea3a94..908d92b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.0 +0.2.0-dev.1 From 711722bb008bc3b9e5864aaccaeaac625ced9ff5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 19 May 2022 15:53:59 +0200 Subject: [PATCH 003/208] fix packaging --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index 6ee1ac1..ef1f906 100644 --- a/setup.py +++ b/setup.py @@ -47,4 +47,12 @@ def get_requirements(file_path: Union[Path, str]): "Programming Language :: Python :: 3.7", ], python_requires='>=3.7', + data_files=[ + ('.', [ + 'version.txt', + 'requirements.txt', + 'requirements-dev.txt', + 'requirements-opt.txt', + ]), + ] ) From b8a2992ac4fad38e6c2fd259ecf08293ee09a659 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 20 May 2022 07:23:47 +0200 Subject: [PATCH 004/208] deleted own copy of radam and used torch implementation instead --- bitorch/optimization/__init__.py | 3 - bitorch/optimization/radam.py | 301 ---------------------- examples/pytorch_lightning/utils/utils.py | 4 +- 3 files changed, 1 insertion(+), 307 deletions(-) delete mode 100644 bitorch/optimization/__init__.py delete mode 100644 bitorch/optimization/radam.py diff --git a/bitorch/optimization/__init__.py b/bitorch/optimization/__init__.py deleted file mode 100644 index b70c7c2..0000000 --- a/bitorch/optimization/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -This submodule contains a custom implementation of the `rectified adam optimizer ` -""" diff --git a/bitorch/optimization/radam.py b/bitorch/optimization/radam.py deleted file mode 100644 index 5630a71..0000000 --- a/bitorch/optimization/radam.py +++ /dev/null @@ -1,301 +0,0 @@ -"""RAdam implementation copied from https://github.com/LiyuanLucasLiu/RAdam/blob/master/radam/radam.py. - -It has been proposed in `On the Variance of the Adaptive Learning Rate and Beyond`. -https://arxiv.org/abs/1908.03265 - -""" - -import math -from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union -import torch -from torch.functional import Tensor -from torch.optim.optimizer import Optimizer - - -class RAdam(Optimizer): - - def __init__( - self, - params: Union[Iterable[Tensor], Iterable[Dict[Any, Any]]], - lr: float = 1e-3, - betas: Tuple[float, float] = (0.9, 0.999), - eps: float = 1e-8, - weight_decay: float = 0, - degenerated_to_sgd: bool = True) -> None: - """Initialises RAdam optimizer - - Args: - params (Union[Iterable[Tensor], Iterable[Dict[Any, Any]]]): iterable of parameters to optimize or dicts - defining parameter groups - lr (float, optional): learning range. Defaults to 1e-3. - betas (Tuple[float, float], optional): coefficients used for computing running averages of gradient and its - square. Defaults to (0.9, 0.999). - eps (float, optional): term added to the denominator to improve numerical stability. Defaults to 1e-8. - weight_decay (float, optional): weight decay (L2 penality). Defaults to 0. - degenerated_to_sgd (bool, optional): toggles wether to use sgd step. Defaults to True. - - Raises: - ValueError: thrown if lr <= 0.0 - ValueError: thrown if eps <= 0.0 - ValueError: thrown if first beta value <= 0 - ValueError: thrown if second beta value <= 0 - """ - if not 0.0 <= lr: - raise ValueError("Invalid learning rate: {}".format(lr)) - if not 0.0 <= eps: - raise ValueError("Invalid epsilon value: {}".format(eps)) - if not 0.0 <= betas[0] < 1.0: - raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) - if not 0.0 <= betas[1] < 1.0: - raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) - - self.degenerated_to_sgd = degenerated_to_sgd - if isinstance(params, (list, tuple)) and len(params) > 0 and isinstance(params[0], dict): - for param in params: - if 'betas' in param and (param['betas'][0] != betas[0] or param['betas'][1] != betas[1]): - param['buffer'] = [[None, None, None] for _ in range(10)] - defaults = dict( - lr=lr, - betas=betas, - eps=eps, - weight_decay=weight_decay, - buffer=[[None, None, None] for _ in range(10)] - ) - super(RAdam, self).__init__(params, defaults) - - def __getstate__(self) -> dict: - # for correct pickling of this class (necessary for mp.spawn) - optimizer_state = super(RAdam, self).__getstate__() # type: ignore - optimizer_state["degenerated_to_sgd"] = self.degenerated_to_sgd - return optimizer_state - - def step(self, closure: Callable = None) -> Optional[float]: - - loss = None - if closure is not None: - loss = closure() - - for group in self.param_groups: - - for p in group['params']: - if p.grad is None: - continue - grad = p.grad.data.float() - if grad.is_sparse: - raise RuntimeError('RAdam does not support sparse gradients') - - p_data_fp32 = p.data.float() - - state = self.state[p] - - if len(state) == 0: - state['step'] = 0 - state['exp_avg'] = torch.zeros_like(p_data_fp32) - state['exp_avg_sq'] = torch.zeros_like(p_data_fp32) - else: - state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32) - state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32) - - exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] - beta1, beta2 = group['betas'] - - exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) - exp_avg.mul_(beta1).add_(1 - beta1, grad) - - state['step'] += 1 - buffered = group['buffer'][int(state['step'] % 10)] - if state['step'] == buffered[0]: - n_sma, step_size = buffered[1], buffered[2] - else: - buffered[0] = state['step'] - beta2_t = beta2 ** state['step'] - n_sma_max = 2 / (1 - beta2) - 1 - n_sma = n_sma_max - 2 * state['step'] * beta2_t / (1 - beta2_t) - buffered[1] = n_sma - - # more conservative since it's an approximated value - if n_sma >= 5: - step_size = math.sqrt( - (1 - beta2_t) * (n_sma - 4) - / (n_sma_max - 4) * (n_sma - 2) - / n_sma * n_sma_max / (n_sma_max - 2)) / (1 - beta1 ** state['step']) - elif self.degenerated_to_sgd: - step_size = 1.0 / (1 - beta1 ** state['step']) - else: - step_size = -1 - buffered[2] = step_size - - # more conservative since it's an approximated value - if n_sma >= 5: - if group['weight_decay'] != 0: - p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32) - denom = exp_avg_sq.sqrt().add_(group['eps']) - p_data_fp32.addcdiv_(-step_size * group['lr'], exp_avg, denom) - p.data.copy_(p_data_fp32) - elif step_size > 0: - if group['weight_decay'] != 0: - p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32) - p_data_fp32.add_(-step_size * group['lr'], exp_avg) - p.data.copy_(p_data_fp32) - - return loss - - -class PlainRAdam(Optimizer): - def __init__( - self, - params: Union[Iterable[Tensor], Iterable[Dict[Any, Any]]], - lr: float = 1e-3, - betas: Tuple[float, float] = (0.9, 0.999), - eps: float = 1e-8, - weight_decay: float = 0, - degenerated_to_sgd: bool = True) -> None: - if not 0.0 <= lr: - raise ValueError("Invalid learning rate: {}".format(lr)) - if not 0.0 <= eps: - raise ValueError("Invalid epsilon value: {}".format(eps)) - if not 0.0 <= betas[0] < 1.0: - raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) - if not 0.0 <= betas[1] < 1.0: - raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) - - self.degenerated_to_sgd = degenerated_to_sgd - defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay) - - super(PlainRAdam, self).__init__(params, defaults) - - def step(self, closure: Callable = None) -> Optional[float]: - - loss = None - if closure is not None: - loss = closure() - - for group in self.param_groups: - - for p in group['params']: - if p.grad is None: - continue - grad = p.grad.data.float() - if grad.is_sparse: - raise RuntimeError('RAdam does not support sparse gradients') - - p_data_fp32 = p.data.float() - - state = self.state[p] - - if len(state) == 0: - state['step'] = 0 - state['exp_avg'] = torch.zeros_like(p_data_fp32) - state['exp_avg_sq'] = torch.zeros_like(p_data_fp32) - else: - state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32) - state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32) - - exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] - beta1, beta2 = group['betas'] - - exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) - exp_avg.mul_(beta1).add_(1 - beta1, grad) - - state['step'] += 1 - beta2_t = beta2 ** state['step'] - n_sma_max = 2 / (1 - beta2) - 1 - n_sma = n_sma_max - 2 * state['step'] * beta2_t / (1 - beta2_t) - - # more conservative since it's an approximated value - if n_sma >= 5: - if group['weight_decay'] != 0: - p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32) - step_size = group['lr'] * math.sqrt( - (1 - beta2_t) * (n_sma - 4) - / (n_sma_max - 4) * (n_sma - 2) - / n_sma * n_sma_max / (n_sma_max - 2)) / (1 - beta1 ** state['step']) - denom = exp_avg_sq.sqrt().add_(group['eps']) - p_data_fp32.addcdiv_(-step_size, exp_avg, denom) - p.data.copy_(p_data_fp32) - elif self.degenerated_to_sgd: - if group['weight_decay'] != 0: - p_data_fp32.add_(-group['weight_decay'] * group['lr'], p_data_fp32) - step_size = group['lr'] / (1 - beta1 ** state['step']) - p_data_fp32.add_(-step_size, exp_avg) - p.data.copy_(p_data_fp32) - - return loss - - -class AdamW(Optimizer): - - def __init__( - self, - params: Union[Iterable[Tensor], Iterable[Dict[Any, Any]]], - lr: float = 1e-3, - betas: Tuple[float, float] = (0.9, 0.999), - eps: float = 1e-8, - weight_decay: float = 0, - warmup: int = 0) -> None: - if not 0.0 <= lr: - raise ValueError("Invalid learning rate: {}".format(lr)) - if not 0.0 <= eps: - raise ValueError("Invalid epsilon value: {}".format(eps)) - if not 0.0 <= betas[0] < 1.0: - raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) - if not 0.0 <= betas[1] < 1.0: - raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) - - defaults = dict(lr=lr, betas=betas, eps=eps, - weight_decay=weight_decay, warmup=warmup) - super(AdamW, self).__init__(params, defaults) - - def step(self, closure: Callable = None) -> Optional[float]: - loss = None - if closure is not None: - loss = closure() - - for group in self.param_groups: - - for p in group['params']: - if p.grad is None: - continue - grad = p.grad.data.float() - if grad.is_sparse: - raise RuntimeError('Adam does not support sparse gradients, please consider SparseAdam instead') - - p_data_fp32 = p.data.float() - - state = self.state[p] - - if len(state) == 0: - state['step'] = 0 - state['exp_avg'] = torch.zeros_like(p_data_fp32) - state['exp_avg_sq'] = torch.zeros_like(p_data_fp32) - else: - state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32) - state['exp_avg_sq'] = state['exp_avg_sq'].type_as(p_data_fp32) - - exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] - beta1, beta2 = group['betas'] - - state['step'] += 1 - - exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad) - exp_avg.mul_(beta1).add_(1 - beta1, grad) - - denom = exp_avg_sq.sqrt().add_(group['eps']) - bias_correction1 = 1 - beta1 ** state['step'] - bias_correction2 = 1 - beta2 ** state['step'] - - if group['warmup'] > state['step']: - scheduled_lr = 1e-8 + state['step'] * group['lr'] / group['warmup'] - else: - scheduled_lr = group['lr'] - - step_size = scheduled_lr * math.sqrt(bias_correction2) / bias_correction1 - - if group['weight_decay'] != 0: - p_data_fp32.add_(-group['weight_decay'] * scheduled_lr, p_data_fp32) - - p_data_fp32.addcdiv_(-step_size, exp_avg, denom) - - p.data.copy_(p_data_fp32) - - return loss diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index 532451e..bb79d5e 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -1,13 +1,11 @@ import logging from pathlib import Path -from torch.optim import Adam, SGD +from torch.optim import Adam, SGD, RAdam from torch.optim.lr_scheduler import MultiStepLR, ExponentialLR, CosineAnnealingLR, _LRScheduler from typing import Union, Optional from torch.nn import Module from torch.optim.optimizer import Optimizer -from bitorch.optimization.radam import RAdam - def set_logging(log_file: Union[None, str], log_level: str, output_stdout: bool) -> None: """configures logging module. From 1d22aff1d650c04a3066a63cbb0579f3ec17afa4 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 24 May 2022 10:36:39 +0200 Subject: [PATCH 005/208] fix error and add changelog --- CHANGELOG.md | 4 ++++ examples/pytorch_lightning/utils/utils.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5ac76..814d4fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.x.x] - Unreleased +### Changed + +- using PyTorch's implementation of RAdam + ## [0.2.0] - 2022/05/19 ### Added diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index bb79d5e..9df6882 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -63,7 +63,7 @@ def create_optimizer(name: str, model: Module, lr: float, momentum: float) -> Op elif name == "sgd": return SGD(params=model.parameters(), lr=lr, momentum=momentum) elif name == "radam": - return RAdam(params=model.parameters(), lr=lr, degenerated_to_sgd=False) + return RAdam(params=model.parameters(), lr=lr) else: raise ValueError(f"No optimizer with name {name} found!") From 17e78a74afe2f822bf6f2c967d9eadea49289c4a Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 31 May 2022 18:27:23 +0200 Subject: [PATCH 006/208] fix protobuf version and reorganize requirements --- CHANGELOG.md | 4 ++++ README.md | 5 ++--- requirements-dev.txt | 2 +- requirements-opt.txt | 2 -- requirements.txt | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 814d4fa..67084c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - using PyTorch's implementation of RAdam +### Fixed + +- fix error from updated protobuf package + ## [0.2.0] - 2022/05/19 ### Added diff --git a/README.md b/README.md index 972f49d..95e1d34 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ If you wish to use a *specific version* of PyTorch for compatibility with certai we advise on installing the corresponding versions of `pytorch` and `torchvision` first (or afterwards), please consult [pytorch's getting started guide](https://pytorch.org/get-started/locally/). -Afterwards simply run: +Otherwise, simply run: ```bash pip install bitorch ``` @@ -32,8 +32,7 @@ Note, that you can also request a specific PyTorch version directly, e.g. for CU pip install bitorch --extra-index-url https://download.pytorch.org/whl/cu113 ``` -To use advanced logging capabilities with [tensorboardX](https://github.com/lanpa/tensorboardX), -install the optional dependencies as well: +To use advanced logging capabilities, install the optional dependencies as well: ```bash pip install "bitorch[opt]" diff --git a/requirements-dev.txt b/requirements-dev.txt index 879ecd8..66e7f4f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ pep8-naming pytest sphinx twine +myst-nb nbclient==0.5.13 nbsphinx==0.8.8 -myst-nb nbsphinx-link==1.3.0 diff --git a/requirements-opt.txt b/requirements-opt.txt index 0b8115e..3969cbf 100644 --- a/requirements-opt.txt +++ b/requirements-opt.txt @@ -1,4 +1,2 @@ -tensorboardX -tensorboard wandb fvbitcore diff --git a/requirements.txt b/requirements.txt index 7cf4b86..7b291b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ matplotlib numpy sklearn pytorch_lightning +protobuf~=3.20.0 torchmetrics From 3333a65626511f32417d15453dcaa981a742b637 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 24 May 2022 14:23:17 +0200 Subject: [PATCH 007/208] remove defaults from arg parser, add custom logger --- .../pytorch_lightning/image_classification.py | 29 ++-- .../logs/csv/version_7/hparams.yaml | 12 ++ ...53394772.Josephs-MaxBuch.fritz.box.13994.0 | Bin 0 -> 2284 bytes .../logs/tensorboard/version_7/hparams.yaml | 12 ++ .../pytorch_lightning/utils/arg_parser.py | 25 ++- .../utils/lightning_model.py | 44 ++++-- examples/pytorch_lightning/utils/log.py | 142 ++++++++++++++++++ examples/pytorch_lightning/utils/utils.py | 1 + 8 files changed, 228 insertions(+), 37 deletions(-) create mode 100644 examples/pytorch_lightning/logs/csv/version_7/hparams.yaml create mode 100644 examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 create mode 100644 examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml create mode 100644 examples/pytorch_lightning/utils/log.py diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index ad6a938..8a454fb 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,5 +1,7 @@ import os +from examples.pytorch_lightning.utils.log import LoggingProgressBar + if os.environ.get('REMOTE_PYCHARM_DEBUG_SESSION', False): import pydevd_pycharm pydevd_pycharm.settrace( @@ -13,7 +15,7 @@ import logging from torch.utils.data import DataLoader from pytorch_lightning import Trainer -from pytorch_lightning.callbacks import ModelCheckpoint +from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger from utils.utils import set_logging from utils.arg_parser import create_argparser @@ -53,10 +55,10 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: apply_args_to_configuration(args) loggers = [] - if args.tensorboard: - loggers.append(TensorBoardLogger(args.tensorboard_output)) # type: ignore - if args.result_file is not None: - loggers.append(CSVLogger(args.result_file)) # type: ignore + if args.tensorboard_log: + loggers.append(TensorBoardLogger(args.result_directory, name="tensorboard")) # type: ignore + if args.csv_log: + loggers.append(CSVLogger(args.result_directory, name="csv")) # type: ignore if WANDB_AVAILABLE and args.wandb: try: loggers.append( @@ -70,6 +72,13 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: if args.checkpoint_dir is not None: callbacks.append(ModelCheckpoint(args.checkpoint_dir, save_last=True, save_top_k=args.checkpoint_keep_count, monitor="metrics/top1 accuracy")) + if not args.enable_progress_bar: + # providing our own progress bar disables the default progress bar (not needed to disable later on) + callbacks.append(LoggingProgressBar(args.log_interval)) + + if len(loggers) > 0: + lr_monitor = LearningRateMonitor(logging_interval='step') + callbacks.append(lr_monitor) dataset = dataset_from_name(args.dataset) @@ -126,11 +135,11 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logging.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) total_flops = stats["#speed up flops (app.)"][""] logging.info("Approximated mflops: " + str(total_flops / 1e6)) - for logger in loggers: - logger.log_dict({ - "mflops": total_flops / 1e6, - "size in MB": total_size / 1e6 / 8.0, - }) + # for logger in loggers: + # logger.log_dict({ + # "mflops": total_flops / 1e6, + # "size in MB": total_size / 1e6 / 8.0, + # }) trainer.fit( model_wrapped, diff --git a/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml b/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml new file mode 100644 index 0000000..6e790dd --- /dev/null +++ b/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml @@ -0,0 +1,12 @@ +add_f1_prec_recall: false +epochs: 2 +lr: 0.001 +lr_factor: 0.1 +lr_scheduler: cosine +lr_steps: +- 30 +- 60 +- 90 +momentum: 0.9 +num_classes: 10 +optimizer: adam diff --git a/examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 b/examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 new file mode 100644 index 0000000000000000000000000000000000000000..edd01f02bca9a715a7c0aa3354aa3f4c7ac2cf14 GIT binary patch literal 2284 zcma))eP|nH9LMj{F3J6xWX;8HC2cgMwQB6Ewc@H=ZyUl`H!ZC?wam-yav_J8T)exK z%{f=7ik3RMVew^kI_q12nd12M+F@ z`##^_^ZoFxC30(OcW;QIH#zR=oSI$hcW+a9DcO44keJlFZ@B-Hxxs*;i?sdRK)rHj zP8X)38mM@RONhEE2{i;U{PFeobsacmh?@q!`GNXs+z@|G!VC#laPbtc@(GP=6St>C zRZ5^48xN82ItdQr7rmdPpCv4k-os*I= zW#a+~7nUXciS(Z(!M7|aZTDC5Q9c1HGkIysdwKegQ=7N-^*~J_INJXDWM*CLfm)mZ z?eyCPMU#>utXf6_op}Da%sSQsesJcI&>p{Qu(Q26coTZov~NXj%K@QNxuA_Kx}-tM z+$31I>i>`x1jPBSx$#?#I(&I_s-I%^8L&E_4gAHZ#AGxF%8LM)g{TbKoaE%lY%A8o z!K0mbXZl)Q8LkEoIApbDEh1N=&?DU&;N9aL#^4u5M&_yd_XhIfMhHP&uCrR3o~e3^ zuB19zctJ?3ys!fm`6Xuf;^Zi1ukPf$eZWV}7xW%*!HR58(cK7Q`sKxLjKqbBZ*$?OViKKadl#@E+INe2 zaTz2c%)qt(-i#Uia;Ag50w zmBb^fo$SHw3mY&J_L!-+kcgiC>oi7!jXLhhlJFIikd9AW!h-O`HF z1G0nxMq-%pPCg#SNW^WXftAFOA&T7-c;f^{!d+qNEhILbd-@VaVvA!c7p}ILMDw9% z{=|aZ^5Ca=aTz2C=IU0m88ZlLB@8eU1hamgei9?`d%?g;LchI<{X2gC3`Sy9HwRis z@cTD!#YnV1weH?*Cj!MJDzBX0g$0>AEab&ykcco#J?U}Gp!cm31{jG5)9~9lFGk{@ df`OI9$sKNX-Rg-YjKqs?n*%K*9;o*@{saEZj;{a! literal 0 HcmV?d00001 diff --git a/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml b/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml new file mode 100644 index 0000000..6e790dd --- /dev/null +++ b/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml @@ -0,0 +1,12 @@ +add_f1_prec_recall: false +epochs: 2 +lr: 0.001 +lr_factor: 0.1 +lr_scheduler: cosine +lr_steps: +- 30 +- 60 +- 90 +momentum: 0.9 +num_classes: 10 +optimizer: adam diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index e4ae057..818f558 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -22,17 +22,16 @@ def add_logging_args(parser: ArgumentParser) -> None: help="how many batches to wait before logging training status") log.add_argument("--log-file", type=str, default=None, help="output file path for logging. default to stdout") - log.add_argument("--log-stdout", action="store_true", default=False, + log.add_argument("--log-stdout", action="store_true", help="toggles force logging to stdout. if a log file is specified, logging will be " "printed to both the log file and stdout") - log.add_argument("--tensorboard", action="store_true", default=False, - help="toggles use of tensorboard for logging learning progress") - log.add_argument("--tensorboard-output", type=str, default="./tblogs", - help="output dir for tensorboard. default to ./tblogs") - log.add_argument("--result-file", type=str, default=None, - help="path to result file; train and test metrics will be logged in csv format") - - log.add_argument("--wandb", action="store_true", default=False, + log.add_argument("--result-directory", type=str, default="./logs", + help="path to logs directory, e.g. tensorboard logs, csv files") + log.add_argument("--disable-tensorboard-log", action="store_false", dest="tensorboard_log", + help="disables tensorboard logging") + log.add_argument("--disable-csv-log", action="store_false", dest="csv_log", + help="disables csv logging") + log.add_argument("--wandb", action="store_true", help="toggles use of wandb for logging learning progress. For this to work, " "the WANDB_API_KEY environment variable must be set.") log.add_argument("--wandb-project", type=str, default="bitorch", @@ -45,11 +44,11 @@ def add_checkpoint_args(parser: ArgumentParser) -> None: checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") checkpoint.add_argument("--checkpoint-dir", type=str, default=None, help="path to directory to store checkpoints in.") - checkpoint.add_argument("--checkpoint-keep-count", type=int, default=10, + checkpoint.add_argument("--checkpoint-keep-count", type=int, default=1, help="number of checkpoints to keep.") checkpoint.add_argument("--checkpoint-load", type=str, default=None, help="path to checkpoint file to load state from. if omitted, a new model will be trained.") - checkpoint.add_argument("--pretrained", action="store_true", default=False, + checkpoint.add_argument("--pretrained", action="store_true", help="uses the given checkpoint as a pretrained model (only for initialization)") @@ -86,7 +85,7 @@ def add_dataset_args(parser: ArgumentParser) -> None: help="name of the dataset to be used for training") data.add_argument("--dataset-dir", type=str, default=None, help="path to where the train dataset is saved / shall be downloaded to") - data.add_argument("--download", action="store_true", default=False, + data.add_argument("--download", action="store_true", help="toggles wether the dataset shall be downloaded if not present. " "only has effect with the cifar10 and mnist dataset so far.") data.add_argument("--batch-size", type=int, default=128, @@ -152,7 +151,7 @@ def add_regular_args(parser: ArgumentParser) -> None: parser.add_argument("--model", type=str.lower, choices=model_names(), required=True, help="name of the model to be trained") - parser.add_argument("--cpu", action="store_true", default=False, + parser.add_argument("--cpu", action="store_true", help="explicitly use the cpu. overwrites gpu settings") diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 6a6452b..0100bc2 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -18,25 +18,34 @@ def __init__( lr_factor: float, lr_steps: list, num_classes: int, - epochs: int) -> None: + epochs: int, + add_f1_prec_recall: bool = False) -> None: super().__init__() self.save_hyperparameters(ignore=["model"]) self.loss_function = CrossEntropyLoss() self.model = model + self.train_accuracy_top1 = Accuracy(num_classes=num_classes) + self.train_accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) self.accuracy_top1 = Accuracy(num_classes=num_classes) self.accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) - self.f1 = F1Score(num_classes=num_classes) - self.prec = Precision(num_classes=num_classes) - self.recall = Recall(num_classes=num_classes) + self.add_f1_prec_recall = add_f1_prec_recall + if add_f1_prec_recall: + self.f1 = F1Score(num_classes=num_classes) + self.prec = Precision(num_classes=num_classes) + self.recall = Recall(num_classes=num_classes) def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore x_train, y_train = batch y_hat = self.model(x_train) loss = self.loss_function(y_hat, y_train) + self.train_accuracy_top1(y_hat, y_train) + self.train_accuracy_top5(y_hat, y_train) self.log_dict({ - "loss/train": loss, - }) + "metrics/train-top1-accuracy": self.accuracy_top1(y_hat, y_train), + "metrics/train-top5-accuracy": self.accuracy_top1(y_hat, y_train), + }, prog_bar=True) + self.log("loss/train", loss) return loss def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: ignore @@ -45,17 +54,24 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: y_hat = self.model(x_test) loss = self.loss_function(y_hat, y_test) - self.log_dict({ - "metrics/top1 accuracy": self.accuracy_top1(y_hat, y_test), - "metrics/top5 accuracy": self.accuracy_top5(y_hat, y_test), - "metrics/f1": self.f1(y_hat, y_test), - "metrics/precision": self.prec(y_hat, y_test), - "metrics/recall": self.recall(y_hat, y_test), + metrics_dict = { + "metrics/test-top1-accuracy": self.accuracy_top1(y_hat, y_test), + "metrics/test-top5-accuracy": self.accuracy_top5(y_hat, y_test), "loss/test": loss, - }, prog_bar=True) + } + + if self.add_f1_prec_recall: + metrics_dict.update({ + "metrics/f1": self.f1(y_hat, y_test), + "metrics/precision": self.prec(y_hat, y_test), + "metrics/recall": self.recall(y_hat, y_test), + }) + self.log_dict(metrics_dict, prog_bar=True) + + return loss def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: ignore - logging.info(f"Using {self.hparams.optimizer} optimizer and {self.hparams.lr_scheduler} lr schedluer...") + logging.info(f"Using {self.hparams.optimizer} optimizer and {self.hparams.lr_scheduler} lr scheduler...") optimizer = create_optimizer(self.hparams.optimizer, self.model, self.hparams.lr, self.hparams.momentum) if self.hparams.lr_scheduler is not None: scheduler = create_scheduler( diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py new file mode 100644 index 0000000..8e072d3 --- /dev/null +++ b/examples/pytorch_lightning/utils/log.py @@ -0,0 +1,142 @@ +import logging +import time + +import math +from pytorch_lightning.callbacks import ProgressBarBase + +TIME_INTERVALS = ( + ('w', 60 * 60 * 24 * 7), + ('d', 60 * 60 * 24), + ('h', 60 * 60), + ('m', 60), + ('s', 1), +) + + +def display_time(seconds, granularity=2): + result = [] + + seconds = int(round(seconds)) + + for name, count in TIME_INTERVALS: + value = seconds // count + if value == 0 and len(result) == 0: + continue + seconds -= value * count + if value == 1: + name = name.rstrip('s') + result.append(f"{value:02d}{name}") + return ':'.join(result[:granularity]) + + +class LoggingProgressBar(ProgressBarBase): + def __init__(self, refresh_rate): + super().__init__() + self._is_enable = True + self._epoch_start_time = None + self._validation_start_time = None + self._train_start_time = None + self._last_epoch_times = [] + self._validation_times = [] + self.refresh_rate = refresh_rate + + def disable(self): + self._is_enable = False + + def enable(self): + self._is_enable = True + + def _should_update(self, current: int, total: int) -> bool: + return self._is_enable and (current % self.refresh_rate == 0 or current == total) + + def on_train_start(self, trainer, pl_module): + logging.info("Training starting.") + + def on_train_end(self, trainer, pl_module): + logging.info("Training ending.") + + def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx: int, unused: int = 0) -> None: + if not self._should_update(self.train_batch_idx, self.total_train_batches): + return + + percent = (self.train_batch_idx / self.total_train_batches) * 100 + + time_in_this_epoch = time.time() - self._epoch_start_time + epoch_total_est = int(round((time_in_this_epoch * self.total_train_batches) / self.train_batch_idx)) + eta_epoch = display_time(epoch_total_est - time_in_this_epoch) + full_epochs_left = trainer.max_epochs - trainer.current_epoch + if full_epochs_left < 0: + full_epochs_left = 0 + if self._average_epoch_time() > 0: + epoch_total_est = self._average_epoch_time() + self._average_validation_time() + eta_train = display_time(epoch_total_est - time_in_this_epoch + full_epochs_left * epoch_total_est) + + epoch_info = f"Epoch {trainer.current_epoch:3d}" + batch_info = f"{self.train_batch_idx:4d}/{self.total_train_batches:4d} ({percent:5.1f}%)" + metrics = self._format_metric_string(self.get_metrics(trainer, pl_module)) + eta_info = f"ETA: {eta_epoch} & {eta_train}" + self.train_batch(f"{epoch_info} - {batch_info} - {metrics} - {eta_info}") + + @staticmethod + def train_batch(message): + logging.info(message) + + @staticmethod + def _replace_metric_key(metric_key): + remove_strings = [ + "metrics/", + "/train", + "train-", + "/test", + "test-", + ] + for s in remove_strings: + metric_key = metric_key.replace(s, "") + return metric_key.replace("accuracy", "acc") + + @staticmethod + def _format_metric_string(metrics_dict): + metric_list = [] + skip_keys = {"v_num"} + + for key, v in metrics_dict.items(): + key = LoggingProgressBar._replace_metric_key(key) + v = float(v) + if math.isnan(v) or key in skip_keys: + continue + if key: + metric_list.append(f"{key}={float(v):2.2f}") + + return ", ".join(metric_list) + + @staticmethod + def _average_time(time_list): + return int(round(sum(time_list) / len(time_list))) + + def _average_epoch_time(self): + if len(self._last_epoch_times) == 0: + return 0 + return self._average_time(self._last_epoch_times) + + def _average_validation_time(self): + if len(self._validation_times) == 0: + return 0 + return self._average_time(self._validation_times) + + def on_train_epoch_start(self, trainer, pl_module) -> None: + self._epoch_start_time = time.time() + if self._train_start_time is None: + self._train_start_time = self._epoch_start_time + + def on_train_epoch_end(self, trainer, pl_module) -> None: + self._last_epoch_times.append(time.time() - self._epoch_start_time) + self._last_epoch_times = self._last_epoch_times[-3:] + + def on_validation_start(self, trainer, pl_module) -> None: + self._validation_start_time = time.time() + logging.info("Validating model...") + + def on_validation_end(self, trainer, pl_module) -> None: + self._validation_times.append(time.time() - self._validation_start_time) + self._validation_times = self._validation_times[-3:] + logging.info(f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module))})") diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index 9df6882..d66d288 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -1,5 +1,6 @@ import logging from pathlib import Path + from torch.optim import Adam, SGD, RAdam from torch.optim.lr_scheduler import MultiStepLR, ExponentialLR, CosineAnnealingLR, _LRScheduler from typing import Union, Optional From 2549c2f46c4cc81c8fbe6674e17240056819a118 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 24 May 2022 15:18:01 +0200 Subject: [PATCH 008/208] update and fix paths and logging --- examples/pytorch_lightning/.gitignore | 1 + .../pytorch_lightning/image_classification.py | 32 +++++++++++------- .../logs/csv/version_7/hparams.yaml | 12 ------- ...53394772.Josephs-MaxBuch.fritz.box.13994.0 | Bin 2284 -> 0 bytes .../logs/tensorboard/version_7/hparams.yaml | 12 ------- .../pytorch_lightning/utils/arg_parser.py | 5 ++- examples/pytorch_lightning/utils/log.py | 18 ++++++---- 7 files changed, 35 insertions(+), 45 deletions(-) create mode 100644 examples/pytorch_lightning/.gitignore delete mode 100644 examples/pytorch_lightning/logs/csv/version_7/hparams.yaml delete mode 100644 examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 delete mode 100644 examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml diff --git a/examples/pytorch_lightning/.gitignore b/examples/pytorch_lightning/.gitignore new file mode 100644 index 0000000..333c1e9 --- /dev/null +++ b/examples/pytorch_lightning/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 8a454fb..81a3493 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from examples.pytorch_lightning.utils.log import LoggingProgressBar @@ -54,15 +55,19 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: apply_args_to_configuration(args) + output_dir = Path(args.result_directory) + output_dir.mkdir(exist_ok=True) + loggers = [] if args.tensorboard_log: - loggers.append(TensorBoardLogger(args.result_directory, name="tensorboard")) # type: ignore + loggers.append(TensorBoardLogger(output_dir, name="tensorboard")) # type: ignore if args.csv_log: - loggers.append(CSVLogger(args.result_directory, name="csv")) # type: ignore - if WANDB_AVAILABLE and args.wandb: + loggers.append(CSVLogger(output_dir, name="csv")) # type: ignore + if WANDB_AVAILABLE and args.wandb_log: try: loggers.append( - WandbLogger(project=args.wandb_project, log_model=True, name=args.wandb_experiment)) # type: ignore + WandbLogger(project=args.wandb_project, log_model=True, name=args.wandb_experiment, save_dir=str(output_dir)) + ) # type: ignore except ModuleNotFoundError: logging.warning( "wandb is not installed, values will not be logged via wandb. install it with " @@ -71,10 +76,10 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: callbacks = [] if args.checkpoint_dir is not None: callbacks.append(ModelCheckpoint(args.checkpoint_dir, save_last=True, - save_top_k=args.checkpoint_keep_count, monitor="metrics/top1 accuracy")) - if not args.enable_progress_bar: - # providing our own progress bar disables the default progress bar (not needed to disable later on) - callbacks.append(LoggingProgressBar(args.log_interval)) + save_top_k=args.checkpoint_keep_count, monitor="metrics/test-top1-accuracy")) + + # providing our own progress bar disables the default progress bar (not needed to disable later on) + callbacks.append(LoggingProgressBar(args.log_interval)) if len(loggers) > 0: lr_monitor = LearningRateMonitor(logging_interval='step') @@ -104,15 +109,18 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: max_steps=args.max_steps, logger=loggers if len(loggers) > 0 else None, # type: ignore callbacks=callbacks, # type: ignore - log_every_n_steps=args.log_interval, - progress_bar_refresh_rate=10, + log_every_n_steps=args.log_interval ) augmentation_level = Augmentation.from_string(args.augmentation) + logging.info(f"model: {args.model}") + logging.info(f"optimizer: {args.optimizer}") + logging.info(f"lr: {args.lr}") + logging.info(f"max_epochs: {args.max_epochs}") if args.fake_data: - logging.info(f"dummy dataset: {dataset.name} (not using real data!)...") + logging.info(f"dummy dataset: {dataset.name} (not using real data!)") train_dataset, test_dataset = dataset.get_dummy_train_and_test_datasets() # type: ignore else: - logging.info(f"dataset: {dataset.name}...") + logging.info(f"dataset: {dataset.name}") train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level ) diff --git a/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml b/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml deleted file mode 100644 index 6e790dd..0000000 --- a/examples/pytorch_lightning/logs/csv/version_7/hparams.yaml +++ /dev/null @@ -1,12 +0,0 @@ -add_f1_prec_recall: false -epochs: 2 -lr: 0.001 -lr_factor: 0.1 -lr_scheduler: cosine -lr_steps: -- 30 -- 60 -- 90 -momentum: 0.9 -num_classes: 10 -optimizer: adam diff --git a/examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 b/examples/pytorch_lightning/logs/tensorboard/version_7/events.out.tfevents.1653394772.Josephs-MaxBuch.fritz.box.13994.0 deleted file mode 100644 index edd01f02bca9a715a7c0aa3354aa3f4c7ac2cf14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2284 zcma))eP|nH9LMj{F3J6xWX;8HC2cgMwQB6Ewc@H=ZyUl`H!ZC?wam-yav_J8T)exK z%{f=7ik3RMVew^kI_q12nd12M+F@ z`##^_^ZoFxC30(OcW;QIH#zR=oSI$hcW+a9DcO44keJlFZ@B-Hxxs*;i?sdRK)rHj zP8X)38mM@RONhEE2{i;U{PFeobsacmh?@q!`GNXs+z@|G!VC#laPbtc@(GP=6St>C zRZ5^48xN82ItdQr7rmdPpCv4k-os*I= zW#a+~7nUXciS(Z(!M7|aZTDC5Q9c1HGkIysdwKegQ=7N-^*~J_INJXDWM*CLfm)mZ z?eyCPMU#>utXf6_op}Da%sSQsesJcI&>p{Qu(Q26coTZov~NXj%K@QNxuA_Kx}-tM z+$31I>i>`x1jPBSx$#?#I(&I_s-I%^8L&E_4gAHZ#AGxF%8LM)g{TbKoaE%lY%A8o z!K0mbXZl)Q8LkEoIApbDEh1N=&?DU&;N9aL#^4u5M&_yd_XhIfMhHP&uCrR3o~e3^ zuB19zctJ?3ys!fm`6Xuf;^Zi1ukPf$eZWV}7xW%*!HR58(cK7Q`sKxLjKqbBZ*$?OViKKadl#@E+INe2 zaTz2c%)qt(-i#Uia;Ag50w zmBb^fo$SHw3mY&J_L!-+kcgiC>oi7!jXLhhlJFIikd9AW!h-O`HF z1G0nxMq-%pPCg#SNW^WXftAFOA&T7-c;f^{!d+qNEhILbd-@VaVvA!c7p}ILMDw9% z{=|aZ^5Ca=aTz2C=IU0m88ZlLB@8eU1hamgei9?`d%?g;LchI<{X2gC3`Sy9HwRis z@cTD!#YnV1weH?*Cj!MJDzBX0g$0>AEab&ykcco#J?U}Gp!cm31{jG5)9~9lFGk{@ df`OI9$sKNX-Rg-YjKqs?n*%K*9;o*@{saEZj;{a! diff --git a/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml b/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml deleted file mode 100644 index 6e790dd..0000000 --- a/examples/pytorch_lightning/logs/tensorboard/version_7/hparams.yaml +++ /dev/null @@ -1,12 +0,0 @@ -add_f1_prec_recall: false -epochs: 2 -lr: 0.001 -lr_factor: 0.1 -lr_scheduler: cosine -lr_steps: -- 30 -- 60 -- 90 -momentum: 0.9 -num_classes: 10 -optimizer: adam diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 818f558..c0f6fd1 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -31,9 +31,8 @@ def add_logging_args(parser: ArgumentParser) -> None: help="disables tensorboard logging") log.add_argument("--disable-csv-log", action="store_false", dest="csv_log", help="disables csv logging") - log.add_argument("--wandb", action="store_true", - help="toggles use of wandb for logging learning progress. For this to work, " - "the WANDB_API_KEY environment variable must be set.") + log.add_argument("--wandb", action="store_true", dest="wandb_log", + help="enables wandb logging (WANDB_API_KEY environment variable must be set)") log.add_argument("--wandb-project", type=str, default="bitorch", help="name of wand project to be used by wandb logger") log.add_argument("--wandb-experiment", type=str, default=None, diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index 8e072d3..ff53153 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -50,10 +50,10 @@ def _should_update(self, current: int, total: int) -> bool: return self._is_enable and (current % self.refresh_rate == 0 or current == total) def on_train_start(self, trainer, pl_module): - logging.info("Training starting.") + logging.info("Starting training...") def on_train_end(self, trainer, pl_module): - logging.info("Training ending.") + logging.info("Ending training.") def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx: int, unused: int = 0) -> None: if not self._should_update(self.train_batch_idx, self.total_train_batches): @@ -101,11 +101,17 @@ def _format_metric_string(metrics_dict): for key, v in metrics_dict.items(): key = LoggingProgressBar._replace_metric_key(key) - v = float(v) - if math.isnan(v) or key in skip_keys: + if key in skip_keys: continue - if key: - metric_list.append(f"{key}={float(v):2.2f}") + try: + v = float(v) + if math.isnan(v): + continue + if key: + metric_list.append(f"{key}={v:2.2f}") + except ValueError: + if key: + metric_list.append(f"{key}={v}") return ", ".join(metric_list) From 81005418d03519d3982fa6bdeebe66ac2fc31cb5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 24 May 2022 17:15:29 +0200 Subject: [PATCH 009/208] fix errors --- examples/pytorch_lightning/image_classification.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 81a3493..15d184f 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,6 +1,8 @@ import os from pathlib import Path +import torch + from examples.pytorch_lightning.utils.log import LoggingProgressBar if os.environ.get('REMOTE_PYCHARM_DEBUG_SESSION', False): @@ -130,10 +132,10 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: shuffle=False, pin_memory=True, persistent_workers=True) # type: ignore if FVBITCORE_AVAILABLE: - data_point = iter(train_loader).next() + data_point = torch.zeros(dataset.shape) computational_intensity = fv_nn.FlopCountAnalysis( model, - inputs=data_point[0], + inputs=data_point, quantization_base_class=Quantization ) From 3609784f37bb47e552cde4733922c426b746e443 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 25 May 2022 13:20:17 +0200 Subject: [PATCH 010/208] try to 'fix' logging with multi GPUs by using print instead --- .../pytorch_lightning/image_classification.py | 35 +++--- examples/pytorch_lightning/utils/__init__.py | 0 .../pytorch_lightning/utils/arg_parser.py | 2 +- examples/pytorch_lightning/utils/log.py | 108 +++++++++++------- examples/pytorch_lightning/utils/utils.py | 7 +- 5 files changed, 91 insertions(+), 61 deletions(-) create mode 100644 examples/pytorch_lightning/utils/__init__.py diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 15d184f..f3f3dab 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -20,7 +20,7 @@ from pytorch_lightning import Trainer from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger -from utils.utils import set_logging +from utils.utils import configure_logging from utils.arg_parser import create_argparser from utils.lightning_model import ModelWrapper @@ -30,11 +30,14 @@ from bitorch import apply_args_to_configuration from bitorch.quantizations import Quantization +_log = logging.getLogger() + + FVBITCORE_AVAILABLE = True try: import fvbitcore.nn as fv_nn except ModuleNotFoundError: - logging.warning("fvbitcore not installed, will not calculate model flops!") + _log.warning("fvbitcore not installed, will not calculate model flops!") FVBITCORE_AVAILABLE = False WANDB_AVAILABLE = True @@ -42,7 +45,7 @@ from pytorch_lightning.loggers import WandbLogger import wandb except ModuleNotFoundError: - logging.warning("wandb not installed, will not log metrics to wandb!") + _log.warning("wandb not installed, will not log metrics to wandb!") WANDB_AVAILABLE = False @@ -53,7 +56,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: args (argparse.Namespace): cli arguments model_args (argparse.Namespace): model specific cli arguments """ - set_logging(args.log_file, args.log_level, args.log_stdout) + configure_logging(_log, args.log_file, args.log_level, args.log_stdout) apply_args_to_configuration(args) @@ -71,7 +74,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: WandbLogger(project=args.wandb_project, log_model=True, name=args.wandb_experiment, save_dir=str(output_dir)) ) # type: ignore except ModuleNotFoundError: - logging.warning( + _log.warning( "wandb is not installed, values will not be logged via wandb. install it with " "`pip install wandb`." ) @@ -90,12 +93,12 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: dataset = dataset_from_name(args.dataset) model_kwargs = vars(model_args) - logging.debug(f"got model args as dict: {model_kwargs}") + _log.debug(f"got model args as dict: {model_kwargs}") model = model_from_name(args.model)(**model_kwargs, dataset=dataset) # type: ignore model.initialize() if args.checkpoint_load is not None and args.pretrained: - logging.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") + _log.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = ModelWrapper( @@ -114,15 +117,15 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: log_every_n_steps=args.log_interval ) augmentation_level = Augmentation.from_string(args.augmentation) - logging.info(f"model: {args.model}") - logging.info(f"optimizer: {args.optimizer}") - logging.info(f"lr: {args.lr}") - logging.info(f"max_epochs: {args.max_epochs}") + _log.info(f"model: {args.model}") + _log.info(f"optimizer: {args.optimizer}") + _log.info(f"lr: {args.lr}") + _log.info(f"max_epochs: {args.max_epochs}") if args.fake_data: - logging.info(f"dummy dataset: {dataset.name} (not using real data!)") + _log.info(f"dummy dataset: {dataset.name} (not using real data!)") train_dataset, test_dataset = dataset.get_dummy_train_and_test_datasets() # type: ignore else: - logging.info(f"dataset: {dataset.name}") + _log.info(f"dataset: {dataset.name}") train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level ) @@ -140,11 +143,11 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) - logging.info("\n" + table) + _log.info("\n" + table) total_size = stats["#compressed size in bits"][""] - logging.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) + _log.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) total_flops = stats["#speed up flops (app.)"][""] - logging.info("Approximated mflops: " + str(total_flops / 1e6)) + _log.info("Approximated mflops: " + str(total_flops / 1e6)) # for logger in loggers: # logger.log_dict({ # "mflops": total_flops / 1e6, diff --git a/examples/pytorch_lightning/utils/__init__.py b/examples/pytorch_lightning/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index c0f6fd1..cd223fa 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -42,7 +42,7 @@ def add_logging_args(parser: ArgumentParser) -> None: def add_checkpoint_args(parser: ArgumentParser) -> None: checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") checkpoint.add_argument("--checkpoint-dir", type=str, default=None, - help="path to directory to store checkpoints in.") + help="set a custom path to store checkpoints in.") checkpoint.add_argument("--checkpoint-keep-count", type=int, default=1, help="number of checkpoints to keep.") checkpoint.add_argument("--checkpoint-load", type=str, default=None, diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index ff53153..43c56b8 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -1,8 +1,13 @@ import logging import time +from typing import Optional, Any, Dict, List import math +import pytorch_lightning as pl from pytorch_lightning.callbacks import ProgressBarBase +from pytorch_lightning.utilities.types import STEP_OUTPUT + +logger = logging.getLogger(__name__) TIME_INTERVALS = ( ('w', 60 * 60 * 24 * 7), @@ -13,8 +18,8 @@ ) -def display_time(seconds, granularity=2): - result = [] +def display_time(seconds: float, granularity: int = 2) -> str: + result: List[str] = [] seconds = int(round(seconds)) @@ -23,39 +28,62 @@ def display_time(seconds, granularity=2): if value == 0 and len(result) == 0: continue seconds -= value * count - if value == 1: - name = name.rstrip('s') result.append(f"{value:02d}{name}") return ':'.join(result[:granularity]) class LoggingProgressBar(ProgressBarBase): - def __init__(self, refresh_rate): + def __init__(self, refresh_rate: int) -> None: super().__init__() - self._is_enable = True - self._epoch_start_time = None - self._validation_start_time = None - self._train_start_time = None - self._last_epoch_times = [] - self._validation_times = [] + self._is_enabled = True + self._epoch_start_time: float = 0.0 + self._validation_start_time: float = 0.0 + self._train_start_time: float = 0.0 + self._last_epoch_times: List[float] = [] + self._validation_times: List[float] = [] self.refresh_rate = refresh_rate + self.log_debug("Logging training progress...") - def disable(self): - self._is_enable = False + @staticmethod + def log_debug(message: str) -> None: + # logger.debug(message) + print(message) - def enable(self): - self._is_enable = True + @staticmethod + def log_info(message: str) -> None: + # logger.info(message) + print(message) - def _should_update(self, current: int, total: int) -> bool: - return self._is_enable and (current % self.refresh_rate == 0 or current == total) + def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: Optional[str] = None) -> None: + self.log_debug(f"Logging setup... ( is root trainer: {trainer.is_global_zero} )") + super().setup(trainer, pl_module, stage) - def on_train_start(self, trainer, pl_module): - logging.info("Starting training...") + def disable(self) -> None: + self.log_debug("Logging disabled...") + self._is_enabled = False - def on_train_end(self, trainer, pl_module): - logging.info("Ending training.") + def enable(self) -> None: + self.log_debug("Logging enabled...") + self._is_enabled = True - def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx: int, unused: int = 0) -> None: + def _should_update(self, current: int, total: int) -> bool: + return self._is_enabled and (current % self.refresh_rate == 0 or current == total) + + def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self.log_info("Starting training...") + + def on_train_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self.log_info("Ending training.") + + def on_train_batch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + outputs: STEP_OUTPUT, + batch: Any, + batch_idx: int, + unused: int = 0, + ) -> None: if not self._should_update(self.train_batch_idx, self.total_train_batches): return @@ -78,11 +106,11 @@ def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx: int, self.train_batch(f"{epoch_info} - {batch_info} - {metrics} - {eta_info}") @staticmethod - def train_batch(message): - logging.info(message) + def train_batch(message: str) -> None: + LoggingProgressBar.log_info(message) @staticmethod - def _replace_metric_key(metric_key): + def _replace_metric_key(metric_key: str) -> str: remove_strings = [ "metrics/", "/train", @@ -95,54 +123,54 @@ def _replace_metric_key(metric_key): return metric_key.replace("accuracy", "acc") @staticmethod - def _format_metric_string(metrics_dict): + def _format_metric_string(metrics_dict: Dict[str, str]) -> str: metric_list = [] skip_keys = {"v_num"} - for key, v in metrics_dict.items(): + for key, value in metrics_dict.items(): key = LoggingProgressBar._replace_metric_key(key) if key in skip_keys: continue try: - v = float(v) - if math.isnan(v): + f_value = float(value) + if math.isnan(f_value): continue if key: - metric_list.append(f"{key}={v:2.2f}") + metric_list.append(f"{key}={f_value:2.2f}") except ValueError: if key: - metric_list.append(f"{key}={v}") + metric_list.append(f"{key}={value}") return ", ".join(metric_list) @staticmethod - def _average_time(time_list): + def _average_time(time_list: List[float]) -> int: return int(round(sum(time_list) / len(time_list))) - def _average_epoch_time(self): + def _average_epoch_time(self) -> int: if len(self._last_epoch_times) == 0: return 0 return self._average_time(self._last_epoch_times) - def _average_validation_time(self): + def _average_validation_time(self) -> int: if len(self._validation_times) == 0: return 0 return self._average_time(self._validation_times) - def on_train_epoch_start(self, trainer, pl_module) -> None: + def on_train_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self._epoch_start_time = time.time() if self._train_start_time is None: self._train_start_time = self._epoch_start_time - def on_train_epoch_end(self, trainer, pl_module) -> None: + def on_train_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self._last_epoch_times.append(time.time() - self._epoch_start_time) self._last_epoch_times = self._last_epoch_times[-3:] - def on_validation_start(self, trainer, pl_module) -> None: + def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self._validation_start_time = time.time() - logging.info("Validating model...") + self.log_info("Validating model...") - def on_validation_end(self, trainer, pl_module) -> None: + def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self._validation_times.append(time.time() - self._validation_start_time) self._validation_times = self._validation_times[-3:] - logging.info(f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module))})") + self.log_info(f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module))})") diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index d66d288..2e023f5 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -3,22 +3,21 @@ from torch.optim import Adam, SGD, RAdam from torch.optim.lr_scheduler import MultiStepLR, ExponentialLR, CosineAnnealingLR, _LRScheduler -from typing import Union, Optional +from typing import Union, Optional, Any from torch.nn import Module from torch.optim.optimizer import Optimizer -def set_logging(log_file: Union[None, str], log_level: str, output_stdout: bool) -> None: +def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, output_stdout: bool) -> None: """configures logging module. Args: + logger: the logger to be configured log_file (str): path to log file. if omitted, logging will be forced to stdout. log_level (str): string name of log level (e.g. 'debug') output_stdout (bool): toggles stdout output. will be activated automatically if no log file was given. otherwise if activated, logging will be outputed both to stdout and log file. """ - logger = logging.getLogger() - log_level_name = log_level.upper() log_level = getattr(logging, log_level_name) logger.setLevel(log_level) From 5198fb1c4dffebe7b0fcb92bd75ddff4ad5eaa68 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 25 May 2022 15:49:01 +0200 Subject: [PATCH 011/208] fix type errors --- .../pytorch_lightning/image_classification.py | 38 +++++++++---------- examples/pytorch_lightning/utils/log.py | 8 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index f3f3dab..0b3dd19 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,10 +1,4 @@ import os -from pathlib import Path - -import torch - -from examples.pytorch_lightning.utils.log import LoggingProgressBar - if os.environ.get('REMOTE_PYCHARM_DEBUG_SESSION', False): import pydevd_pycharm pydevd_pycharm.settrace( @@ -16,10 +10,15 @@ import argparse import logging +from pathlib import Path +from typing import List + +import torch + from torch.utils.data import DataLoader from pytorch_lightning import Trainer from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor -from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger +from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase from utils.utils import configure_logging from utils.arg_parser import create_argparser from utils.lightning_model import ModelWrapper @@ -30,6 +29,8 @@ from bitorch import apply_args_to_configuration from bitorch.quantizations import Quantization +from examples.pytorch_lightning.utils.log import LoggingProgressBar + _log = logging.getLogger() @@ -63,21 +64,20 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: output_dir = Path(args.result_directory) output_dir.mkdir(exist_ok=True) - loggers = [] + loggers: List[LightningLoggerBase] = [] if args.tensorboard_log: - loggers.append(TensorBoardLogger(output_dir, name="tensorboard")) # type: ignore + loggers.append(TensorBoardLogger(str(output_dir), name="tensorboard")) if args.csv_log: - loggers.append(CSVLogger(output_dir, name="csv")) # type: ignore + loggers.append(CSVLogger(str(output_dir), name="csv")) if WANDB_AVAILABLE and args.wandb_log: - try: - loggers.append( - WandbLogger(project=args.wandb_project, log_model=True, name=args.wandb_experiment, save_dir=str(output_dir)) - ) # type: ignore - except ModuleNotFoundError: - _log.warning( - "wandb is not installed, values will not be logged via wandb. install it with " - "`pip install wandb`." + loggers.append( + WandbLogger( + project=args.wandb_project, + log_model=True, + name=args.wandb_experiment, + save_dir=str(output_dir) ) + ) callbacks = [] if args.checkpoint_dir is not None: callbacks.append(ModelCheckpoint(args.checkpoint_dir, save_last=True, @@ -88,7 +88,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: if len(loggers) > 0: lr_monitor = LearningRateMonitor(logging_interval='step') - callbacks.append(lr_monitor) + callbacks.append(lr_monitor) # type: ignore dataset = dataset_from_name(args.dataset) diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index 43c56b8..287cb9b 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -1,6 +1,6 @@ import logging import time -from typing import Optional, Any, Dict, List +from typing import Optional, Any, Dict, List, Union import math import pytorch_lightning as pl @@ -66,8 +66,8 @@ def enable(self) -> None: self.log_debug("Logging enabled...") self._is_enabled = True - def _should_update(self, current: int, total: int) -> bool: - return self._is_enabled and (current % self.refresh_rate == 0 or current == total) + def _should_update(self, current: int, total: Union[int, float]) -> bool: + return self._is_enabled and (current % self.refresh_rate == 0 or current == int(total)) def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self.log_info("Starting training...") @@ -123,7 +123,7 @@ def _replace_metric_key(metric_key: str) -> str: return metric_key.replace("accuracy", "acc") @staticmethod - def _format_metric_string(metrics_dict: Dict[str, str]) -> str: + def _format_metric_string(metrics_dict: Dict[str, Union[int, str]]) -> str: metric_list = [] skip_keys = {"v_num"} From 87569cf6534590c570f8fb230cc479ee56dc4d31 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 27 May 2022 15:24:09 +0200 Subject: [PATCH 012/208] check if logging works with separate logger --- .../pytorch_lightning/image_classification.py | 36 ++++++++-------- examples/pytorch_lightning/utils/log.py | 41 ++++++++++--------- examples/pytorch_lightning/utils/utils.py | 2 - 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 0b3dd19..873acb8 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -29,16 +29,16 @@ from bitorch import apply_args_to_configuration from bitorch.quantizations import Quantization -from examples.pytorch_lightning.utils.log import LoggingProgressBar +from examples.pytorch_lightning.utils.log import CommandLineLogger -_log = logging.getLogger() +logger = logging.getLogger() FVBITCORE_AVAILABLE = True try: import fvbitcore.nn as fv_nn except ModuleNotFoundError: - _log.warning("fvbitcore not installed, will not calculate model flops!") + logger.warning("fvbitcore not installed, will not calculate model flops!") FVBITCORE_AVAILABLE = False WANDB_AVAILABLE = True @@ -46,7 +46,7 @@ from pytorch_lightning.loggers import WandbLogger import wandb except ModuleNotFoundError: - _log.warning("wandb not installed, will not log metrics to wandb!") + logger.warning("wandb not installed, will not log metrics to wandb!") WANDB_AVAILABLE = False @@ -57,7 +57,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: args (argparse.Namespace): cli arguments model_args (argparse.Namespace): model specific cli arguments """ - configure_logging(_log, args.log_file, args.log_level, args.log_stdout) + configure_logging(logger, args.log_file, args.log_level, args.log_stdout) apply_args_to_configuration(args) @@ -84,7 +84,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: save_top_k=args.checkpoint_keep_count, monitor="metrics/test-top1-accuracy")) # providing our own progress bar disables the default progress bar (not needed to disable later on) - callbacks.append(LoggingProgressBar(args.log_interval)) + cmd_logger = CommandLineLogger(args.log_interval) + callbacks.append(cmd_logger) + configure_logging(cmd_logger.logger, args.log_file, args.log_level, args.log_stdout) if len(loggers) > 0: lr_monitor = LearningRateMonitor(logging_interval='step') @@ -93,12 +95,12 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: dataset = dataset_from_name(args.dataset) model_kwargs = vars(model_args) - _log.debug(f"got model args as dict: {model_kwargs}") + logger.debug(f"got model args as dict: {model_kwargs}") model = model_from_name(args.model)(**model_kwargs, dataset=dataset) # type: ignore model.initialize() if args.checkpoint_load is not None and args.pretrained: - _log.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") + logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = ModelWrapper( @@ -117,15 +119,15 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: log_every_n_steps=args.log_interval ) augmentation_level = Augmentation.from_string(args.augmentation) - _log.info(f"model: {args.model}") - _log.info(f"optimizer: {args.optimizer}") - _log.info(f"lr: {args.lr}") - _log.info(f"max_epochs: {args.max_epochs}") + logger.info(f"model: {args.model}") + logger.info(f"optimizer: {args.optimizer}") + logger.info(f"lr: {args.lr}") + logger.info(f"max_epochs: {args.max_epochs}") if args.fake_data: - _log.info(f"dummy dataset: {dataset.name} (not using real data!)") + logger.info(f"dummy dataset: {dataset.name} (not using real data!)") train_dataset, test_dataset = dataset.get_dummy_train_and_test_datasets() # type: ignore else: - _log.info(f"dataset: {dataset.name}") + logger.info(f"dataset: {dataset.name}") train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level ) @@ -143,11 +145,11 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) - _log.info("\n" + table) + logger.info("\n" + table) total_size = stats["#compressed size in bits"][""] - _log.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) + logger.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) total_flops = stats["#speed up flops (app.)"][""] - _log.info("Approximated mflops: " + str(total_flops / 1e6)) + logger.info("Approximated mflops: " + str(total_flops / 1e6)) # for logger in loggers: # logger.log_dict({ # "mflops": total_flops / 1e6, diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index 287cb9b..b3a0515 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -7,7 +7,6 @@ from pytorch_lightning.callbacks import ProgressBarBase from pytorch_lightning.utilities.types import STEP_OUTPUT -logger = logging.getLogger(__name__) TIME_INTERVALS = ( ('w', 60 * 60 * 24 * 7), @@ -32,7 +31,11 @@ def display_time(seconds: float, granularity: int = 2) -> str: return ':'.join(result[:granularity]) -class LoggingProgressBar(ProgressBarBase): +class CommandLineLogger(ProgressBarBase): + """ + This module provides a replacement for the default tqdm-based progress bar, that is more suitable for logging + progress in a non-interactive way, e.g. to a file. + """ def __init__(self, refresh_rate: int) -> None: super().__init__() self._is_enabled = True @@ -41,21 +44,25 @@ def __init__(self, refresh_rate: int) -> None: self._train_start_time: float = 0.0 self._last_epoch_times: List[float] = [] self._validation_times: List[float] = [] + + self.logger = logging.getLogger("CommandLineLogger") + self.refresh_rate = refresh_rate - self.log_debug("Logging training progress...") + self.log_batch = self.log_info + self.log_debug("Command line logger initialized.") - @staticmethod - def log_debug(message: str) -> None: - # logger.debug(message) - print(message) + def log_debug(self, message: str) -> None: + if self._is_enabled: + self.logger.debug(message) + print(message) - @staticmethod - def log_info(message: str) -> None: - # logger.info(message) - print(message) + def log_info(self, message: str) -> None: + if self._is_enabled: + self.logger.info(message) + print(message) def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: Optional[str] = None) -> None: - self.log_debug(f"Logging setup... ( is root trainer: {trainer.is_global_zero} )") + self.log_debug(f"Command line logger setup. ( is root trainer: {trainer.is_global_zero} )") super().setup(trainer, pl_module, stage) def disable(self) -> None: @@ -63,8 +70,8 @@ def disable(self) -> None: self._is_enabled = False def enable(self) -> None: - self.log_debug("Logging enabled...") self._is_enabled = True + self.log_debug("Logging enabled...") def _should_update(self, current: int, total: Union[int, float]) -> bool: return self._is_enabled and (current % self.refresh_rate == 0 or current == int(total)) @@ -103,11 +110,7 @@ def on_train_batch_end( batch_info = f"{self.train_batch_idx:4d}/{self.total_train_batches:4d} ({percent:5.1f}%)" metrics = self._format_metric_string(self.get_metrics(trainer, pl_module)) eta_info = f"ETA: {eta_epoch} & {eta_train}" - self.train_batch(f"{epoch_info} - {batch_info} - {metrics} - {eta_info}") - - @staticmethod - def train_batch(message: str) -> None: - LoggingProgressBar.log_info(message) + self.log_batch(f"{epoch_info} - {batch_info} - {metrics} - {eta_info}") @staticmethod def _replace_metric_key(metric_key: str) -> str: @@ -128,7 +131,7 @@ def _format_metric_string(metrics_dict: Dict[str, Union[int, str]]) -> str: skip_keys = {"v_num"} for key, value in metrics_dict.items(): - key = LoggingProgressBar._replace_metric_key(key) + key = CommandLineLogger._replace_metric_key(key) if key in skip_keys: continue try: diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index 2e023f5..e913eb0 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -30,7 +30,6 @@ def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, o log_file_path = Path(log_file) log_file_path.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(log_file_path) - file_handler.setLevel(log_level) file_handler.setFormatter(logging_format) logger.addHandler(file_handler) else: @@ -38,7 +37,6 @@ def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, o if output_stdout: stream = logging.StreamHandler() - stream.setLevel(log_level) stream.setFormatter(logging_format) logger.addHandler(stream) From 8b8cb44748f0648d3bf20c63c66f047075ed7d87 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 31 May 2022 17:28:27 +0200 Subject: [PATCH 013/208] log (almost) all args --- bitorch/models/resnet_e.py | 3 +- .../pytorch_lightning/image_classification.py | 5 +- .../utils/lightning_model.py | 22 +++--- examples/pytorch_lightning/utils/log.py | 18 +++-- .../pytorch_lightning/utils/unused_args.py | 72 +++++++++++++++++++ examples/pytorch_lightning/utils/utils.py | 1 + 6 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 examples/pytorch_lightning/utils/unused_args.py diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 9a0b429..30f71ac 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -61,8 +61,7 @@ def _build_body(self) -> nn.Sequential: nn.Sequential: the basic building block body model """ return nn.Sequential( - QConv2d(self.in_channels, self.out_channels, kernel_size=3, stride=self.stride, padding=1, bias=False, - input_quantization="sign", weight_quantization="sign"), + QConv2d(self.in_channels, self.out_channels, kernel_size=3, stride=self.stride, padding=1, bias=False), nn.BatchNorm2d(self.out_channels, momentum=0.9), ) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 873acb8..41e8089 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -103,10 +103,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: - model_wrapped = ModelWrapper( - model, args.optimizer, args.lr, args.momentum, args.lr_scheduler, args.lr_factor, args.lr_steps, - dataset.num_classes, args.max_epochs, - ) + model_wrapped = ModelWrapper(model, dataset.num_classes, args) trainer = Trainer( strategy=args.strategy, diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 0100bc2..f6a3149 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -1,27 +1,25 @@ +import logging +from argparse import Namespace from typing import Union + import torch from pytorch_lightning import LightningModule from torch.nn import Module, CrossEntropyLoss -from utils.utils import create_optimizer, create_scheduler -from torchmetrics import Accuracy, F1Score, Precision, Recall, AUROC -import logging +from torchmetrics import Accuracy, F1Score, Precision, Recall + +from .unused_args import clean_hyperparameters +from .utils import create_optimizer, create_scheduler class ModelWrapper(LightningModule): def __init__( self, model: Module, - optimizer: str, - lr: float, - momentum: float, - lr_scheduler: str, - lr_factor: float, - lr_steps: list, num_classes: int, - epochs: int, + args: Namespace, add_f1_prec_recall: bool = False) -> None: super().__init__() - self.save_hyperparameters(ignore=["model"]) + self.save_hyperparameters(clean_hyperparameters(args)) self.loss_function = CrossEntropyLoss() self.model = model self.train_accuracy_top1 = Accuracy(num_classes=num_classes) @@ -76,7 +74,7 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i if self.hparams.lr_scheduler is not None: scheduler = create_scheduler( self.hparams.lr_scheduler, optimizer, self.hparams.lr_factor, - self.hparams.lr_steps, self.hparams.epochs + self.hparams.lr_steps, self.hparams.max_epochs ) return { "optimizer": optimizer, diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index b3a0515..e5fe308 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -53,12 +53,12 @@ def __init__(self, refresh_rate: int) -> None: def log_debug(self, message: str) -> None: if self._is_enabled: - self.logger.debug(message) + # self.logger.debug(message) print(message) def log_info(self, message: str) -> None: if self._is_enabled: - self.logger.info(message) + # self.logger.info(message) print(message) def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: Optional[str] = None) -> None: @@ -126,14 +126,20 @@ def _replace_metric_key(metric_key: str) -> str: return metric_key.replace("accuracy", "acc") @staticmethod - def _format_metric_string(metrics_dict: Dict[str, Union[int, str]]) -> str: + def _format_metric_string(metrics_dict: Dict[str, Union[int, str]], train: bool = True) -> str: metric_list = [] skip_keys = {"v_num"} + if train: + skip_keys.add("test-top1-acc") + skip_keys.add("test-top5-acc") + else: + skip_keys.add("train-top1-acc") + skip_keys.add("train-top5-acc") for key, value in metrics_dict.items(): - key = CommandLineLogger._replace_metric_key(key) if key in skip_keys: continue + key = CommandLineLogger._replace_metric_key(key) try: f_value = float(value) if math.isnan(f_value): @@ -176,4 +182,6 @@ def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningMod def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: self._validation_times.append(time.time() - self._validation_start_time) self._validation_times = self._validation_times[-3:] - self.log_info(f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module))})") + self.log_info( + f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module), train=False)})" + ) diff --git a/examples/pytorch_lightning/utils/unused_args.py b/examples/pytorch_lightning/utils/unused_args.py new file mode 100644 index 0000000..e92455e --- /dev/null +++ b/examples/pytorch_lightning/utils/unused_args.py @@ -0,0 +1,72 @@ +"""Args from PyTorch Lightning's Trainer that are currently unused and a function to deal with them.""" +from argparse import Namespace +from typing import List + + +def clean_hyperparameters(args: Namespace) -> Namespace: + clean_args = Namespace() + for key in args.__dict__.keys(): + if key in UNUSED_PL_ARGS: + continue + setattr(clean_args, key, getattr(args, key)) + return clean_args + + +# this list is copied from the constructor of PyTorch's Trainer, but all arguments used in our script were removed +UNUSED_PL_ARGS: List[str] = [ + "logger", + "checkpoint_callback", + "enable_checkpointing", + "callbacks", + "default_root_dir", + "gradient_clip_val", + "gradient_clip_algorithm", + "process_position", + "num_nodes", + "num_processes", + "devices", + "auto_select_gpus", + "tpu_cores", + "ipus", + "log_gpu_memory", + "progress_bar_refresh_rate", + "enable_progress_bar", + "overfit_batches", + "track_grad_norm", + "check_val_every_n_epoch", + "fast_dev_run", + "accumulate_grad_batches", + "min_epochs", + "min_steps", + "max_time", + "limit_train_batches", + "limit_val_batches", + "limit_test_batches", + "limit_predict_batches", + "val_check_interval", + "flush_logs_every_n_steps", + "log_every_n_steps", + "sync_batchnorm", + "precision", + "enable_model_summary", + "weights_summary", + "weights_save_path", + "num_sanity_val_steps", + "resume_from_checkpoint", + "profiler", + "benchmark", + "deterministic", + "reload_dataloaders_every_n_epochs", + "auto_lr_find", + "replace_sampler_ddp", + "detect_anomaly", + "auto_scale_batch_size", + "prepare_data_per_node", + "plugins", + "amp_backend", + "amp_level", + "move_metrics_to_cpu", + "multiple_trainloader_mode", + "stochastic_weight_avg", + "terminate_on_nan", +] diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index e913eb0..978d5b7 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -56,6 +56,7 @@ def create_optimizer(name: str, model: Module, lr: float, momentum: float) -> Op Returns: Optimizer: the model optimizer """ + name = name.lower() if name == "adam": return Adam(params=model.parameters(), lr=lr) elif name == "sgd": From e80e17b90c217c22012d66b377f9628691603054 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 31 May 2022 13:47:29 +0200 Subject: [PATCH 014/208] adding first simple mnist example --- bitorch/datasets/base.py | 2 +- examples/mnist/train_mnist.py | 173 ++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/mnist/train_mnist.py diff --git a/bitorch/datasets/base.py b/bitorch/datasets/base.py index 9acf68d..35ee0fa 100644 --- a/bitorch/datasets/base.py +++ b/bitorch/datasets/base.py @@ -65,7 +65,7 @@ def __init__( @classmethod def get_train_and_test( cls, - root_directory: str, + root_directory: Optional[str] = None, download: bool = False, augmentation: Augmentation = Augmentation.DEFAULT) -> Tuple["BasicDataset", "BasicDataset"]: """creates a pair of train and test dataset. diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py new file mode 100644 index 0000000..c95fc3f --- /dev/null +++ b/examples/mnist/train_mnist.py @@ -0,0 +1,173 @@ +""" +Adapted from official PyTorch example: https://github.com/pytorch/examples/blob/main/mnist/main.py +""" + +from __future__ import print_function +import argparse +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torch.optim.lr_scheduler import StepLR + +from bitorch import datasets as bitorch_datasets +import bitorch.layers as qnn + + +class QuantizedMLP(nn.Module): + def __init__(self, num_hidden_units_1=128, num_hidden_units_2=64): + super().__init__() + self.flatten = nn.Flatten() + self.fc1 = qnn.QLinear(784, num_hidden_units_1) + self.act1 = nn.PReLU() + self.bn1 = nn.BatchNorm1d(num_hidden_units_1) + + self.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2) + self.act2 = nn.PReLU() + self.bn2 = nn.BatchNorm1d(num_hidden_units_2) + + self.fc3 = qnn.QLinear(num_hidden_units_2, 10) + + def forward(self, x): + x = self.flatten(x) + + x = self.fc1(x) + x = self.act1(x) + x = self.bn1(x) + + x = self.fc2(x) + x = self.act2(x) + x = self.bn2(x) + + x = self.fc3(x) + output = F.log_softmax(x, dim=1) + return output + + +class Net(nn.Module): + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(1, 32, 3, 1) + self.conv2 = nn.Conv2d(32, 64, 3, 1) + self.dropout1 = nn.Dropout(0.25) + self.dropout2 = nn.Dropout(0.5) + self.fc1 = nn.Linear(9216, 128) + self.fc2 = nn.Linear(128, 10) + + def forward(self, x): + x = self.conv1(x) + x = F.relu(x) + x = self.conv2(x) + x = F.relu(x) + x = F.max_pool2d(x, 2) + x = self.dropout1(x) + x = torch.flatten(x, 1) + x = self.fc1(x) + x = F.relu(x) + x = self.dropout2(x) + x = self.fc2(x) + output = F.log_softmax(x, dim=1) + return output + + +def train(args, model, device, train_loader, optimizer, epoch): + model.train() + for batch_idx, (data, target) in enumerate(train_loader): + data, target = data.to(device), target.to(device) + optimizer.zero_grad() + output = model(data) + loss = F.nll_loss(output, target) + loss.backward() + optimizer.step() + if batch_idx % args.log_interval == 0: + print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( + epoch, batch_idx * len(data), len(train_loader.dataset), + 100. * batch_idx / len(train_loader), loss.item())) + if args.dry_run: + break + + +def test(model, device, test_loader): + model.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for data, target in test_loader: + data, target = data.to(device), target.to(device) + output = model(data) + test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss + pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability + correct += pred.eq(target.view_as(pred)).sum().item() + + test_loss /= len(test_loader.dataset) + + print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( + test_loss, correct, len(test_loader.dataset), + 100. * correct / len(test_loader.dataset))) + + +def main(): + # Training settings + parser = argparse.ArgumentParser(description='PyTorch MNIST Example') + parser.add_argument('--model', type=str, choices=["mlp", "lenet"], default="lenet", + help='input batch size for training (default: 64)') + parser.add_argument('--batch-size', type=int, default=64, metavar='N', + help='input batch size for training (default: 64)') + parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N', + help='input batch size for testing (default: 1000)') + parser.add_argument('--epochs', type=int, default=14, metavar='N', + help='number of epochs to train (default: 14)') + parser.add_argument('--lr', type=float, default=1.0, metavar='LR', + help='learning rate (default: 1.0)') + parser.add_argument('--gamma', type=float, default=0.7, metavar='M', + help='Learning rate step gamma (default: 0.7)') + parser.add_argument('--no-cuda', action='store_true', default=False, + help='disables CUDA training') + parser.add_argument('--dry-run', action='store_true', default=False, + help='quickly check a single pass') + parser.add_argument('--seed', type=int, default=1, metavar='S', + help='random seed (default: 1)') + parser.add_argument('--log-interval', type=int, default=10, metavar='N', + help='how many batches to wait before logging training status') + parser.add_argument('--save-model', action='store_true', default=False, + help='For Saving the current Model') + args = parser.parse_args() + + use_cuda = not args.no_cuda and torch.cuda.is_available() + + torch.manual_seed(args.seed) + + device = torch.device("cuda" if use_cuda else "cpu") + + train_kwargs = {'batch_size': args.batch_size} + test_kwargs = {'batch_size': args.test_batch_size} + if use_cuda: + cuda_kwargs = {'num_workers': 1, + 'pin_memory': True, + 'shuffle': True} + train_kwargs.update(cuda_kwargs) + test_kwargs.update(cuda_kwargs) + + train_dataset, test_dataset = bitorch_datasets.MNIST.get_train_and_test(download=True) + + train_loader = torch.utils.data.DataLoader(train_dataset, **train_kwargs) + test_loader = torch.utils.data.DataLoader(test_dataset, **test_kwargs) + + if args.model == "mlp": + model = QuantizedMLP().to(device) + else: + model = Net().to(device) + optimizer = optim.Adadelta(model.parameters(), lr=args.lr) + + scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma) + for epoch in range(1, args.epochs + 1): + train(args, model, device, train_loader, optimizer, epoch) + test(model, device, test_loader) + scheduler.step() + + if args.save_model: + torch.save(model.state_dict(), "mnist_cnn.pt") + + +if __name__ == '__main__': + main() From 99c973671558e25abe867475e40dcbb08f9cdf55 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 1 Jun 2022 16:59:24 +0200 Subject: [PATCH 015/208] implement differing layer implementation depending on selected bitorch mode --- bitorch/__init__.py | 4 ++ bitorch/layers/layer_registry.py | 51 ++++++++++++++++++++++ bitorch/layers/qlinear.py | 20 +++++++-- bitorch/runtime_mode.py | 66 ++++++++++++++++++++++++++++ examples/mnist/train_mnist.py | 6 +-- mypy.ini | 2 +- tests/layers/test_layer_impl.py | 57 ++++++++++++++++++++++++ tests/models/__init__.py | 0 tests/test_mode.py | 74 ++++++++++++++++++++++++++++++++ 9 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 bitorch/layers/layer_registry.py create mode 100644 bitorch/runtime_mode.py create mode 100644 tests/layers/test_layer_impl.py create mode 100644 tests/models/__init__.py create mode 100644 tests/test_mode.py diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 087564f..17c8282 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -5,6 +5,10 @@ from typing import List from .config import Config +from .runtime_mode import RuntimeMode + + +mode: RuntimeMode = RuntimeMode.DEFAULT configs_by_name = {} diff --git a/bitorch/layers/layer_registry.py b/bitorch/layers/layer_registry.py new file mode 100644 index 0000000..ac1442c --- /dev/null +++ b/bitorch/layers/layer_registry.py @@ -0,0 +1,51 @@ +from typing import Set, Optional, Any + +import bitorch +from .. import RuntimeMode +from ..runtime_mode import runtime_mode_type + + +class LayerRegistry: + def __init__(self, name: str) -> None: + self.name = name + self.registered_layers: Set[_LayerImplementation] = set() + + def register(self, layer: "_LayerImplementation") -> None: + self.registered_layers.add(layer) + + def get_layer(self, mode: Optional[RuntimeMode] = None) -> "_LayerImplementation": + if mode is None: + mode = bitorch.mode + available_layers = [] + for layer in self.registered_layers: + if mode.is_supported_by(layer.supports_modes): + available_layers.append(layer) + if len(available_layers) > 1: + RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") + if len(available_layers) == 0: + raise RuntimeError(f"No layer implementation for '{self.name}' available (mode='{mode}').") + return available_layers[0] + + +class _LayerImplementation: + def __init__(self, registry: LayerRegistry, supports_modes: runtime_mode_type) -> None: + self.registry = registry + self.supports_modes = supports_modes + assert self.supports_modes > 0, "Invalid mode given" + self.__initialized = False + self.class_: Any = None + self.class_name = "" + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + if not self.__initialized: + self.__initialized = True + self.class_ = args[0] + self.class_name = self.class_.__name__ + self.registry.register(self) + return self + current_layer = self.registry.get_layer() + if self == current_layer: + # this class provides the correct implementation for the current mode (recursion stop) + return self.class_(*args, **kwargs) + # call this method again but on the correct base class + return current_layer(*args, **kwargs) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 301ec2f..d41c8d9 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -1,16 +1,27 @@ """Module containting the quantized linear layer""" from typing import Union + import torch from torch.nn import Linear from torch.nn.functional import linear -from bitorch.quantizations import Quantization from .config import config +from .layer_registry import LayerRegistry, _LayerImplementation from .qactivation import QActivation +from ..quantizations import Quantization +from ..runtime_mode import RuntimeMode, runtime_mode_type + +_registry = LayerRegistry("QLinear") + +class QLinearImplementation(_LayerImplementation): + def __init__(self, supports_modes: runtime_mode_type) -> None: + super().__init__(_registry, supports_modes) -class QLinear(Linear): + +@QLinearImplementation(RuntimeMode.DEFAULT) +class QLinearDefault(Linear): def __init__( self, *args: int, @@ -31,7 +42,7 @@ def __init__( **kwargs (keyword Argument list): keyword arguments for linear layer """ - super(QLinear, self).__init__(*args, **kwargs) # type: ignore + super().__init__(*args, **kwargs) # type: ignore self.weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) @@ -46,3 +57,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: """ return linear(self.activation(x), self.weight_quantize(self.weight), self.bias) + + +QLinear = QLinearDefault diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py new file mode 100644 index 0000000..fcf42e8 --- /dev/null +++ b/bitorch/runtime_mode.py @@ -0,0 +1,66 @@ +from enum import Enum +from typing import Union, Any +from functools import total_ordering + + +runtime_mode_type = Union["RuntimeMode", int] + + +@total_ordering +class RuntimeMode(Enum): + DEFAULT = 1 + TRAIN = 2 + CPP_INFERENCE = 4 + GPU_INFERENCE = 8 + + def __add__(self, other: runtime_mode_type) -> runtime_mode_type: + if self._to_int(self) == self._to_int(other): + return self + return self._to_int(other) + self.value + + @staticmethod + def _max_val() -> int: + return sum(map(lambda x: x.value, RuntimeMode.__members__.values())) + + @staticmethod + def is_single_mode(mode: runtime_mode_type) -> bool: + return any(x.value == mode for x in RuntimeMode.__members__.values()) + + @staticmethod + def is_combined_mode(mode: runtime_mode_type) -> bool: + return 0 < mode < RuntimeMode._max_val() + + def __lt__(self, other: Any) -> bool: + if not isinstance(other, RuntimeMode) and not isinstance(other, int): + return NotImplemented + return self.value < self._to_int(other) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, RuntimeMode) and not isinstance(other, int): + return NotImplemented + return self.value == self._to_int(other) + + def __str__(self) -> str: + return self.name.lower() + + @staticmethod + def _to_int(mode: runtime_mode_type) -> int: + if isinstance(mode, RuntimeMode): + return mode.value + return mode + + @staticmethod + def from_string(level: str) -> "RuntimeMode": + return { + "default": RuntimeMode.DEFAULT, + "train": RuntimeMode.TRAIN, + "cpp_inference": RuntimeMode.CPP_INFERENCE, + "gpu_inference": RuntimeMode.GPU_INFERENCE, + }[level.lower()] + + @staticmethod + def mode_compatible(required_mode: "RuntimeMode", provided_modes: runtime_mode_type) -> bool: + return bool(RuntimeMode._to_int(required_mode) & RuntimeMode._to_int(provided_modes)) + + def is_supported_by(self, provided_modes: runtime_mode_type) -> bool: + return self.mode_compatible(self, provided_modes) diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index c95fc3f..af5aa2e 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -121,15 +121,15 @@ def main(): help='learning rate (default: 1.0)') parser.add_argument('--gamma', type=float, default=0.7, metavar='M', help='Learning rate step gamma (default: 0.7)') - parser.add_argument('--no-cuda', action='store_true', default=False, + parser.add_argument('--no-cuda', action='store_true', help='disables CUDA training') - parser.add_argument('--dry-run', action='store_true', default=False, + parser.add_argument('--dry-run', action='store_true', help='quickly check a single pass') parser.add_argument('--seed', type=int, default=1, metavar='S', help='random seed (default: 1)') parser.add_argument('--log-interval', type=int, default=10, metavar='N', help='how many batches to wait before logging training status') - parser.add_argument('--save-model', action='store_true', default=False, + parser.add_argument('--save-model', action='store_true', help='For Saving the current Model') args = parser.parse_args() diff --git a/mypy.ini b/mypy.ini index e039b64..9084fb0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,7 +8,7 @@ ignore_missing_imports = True disallow_untyped_defs = True disallow_any_explicit = False disable_error_code = attr-defined - +exclude = examples/mnist [mypy-torchvision.io._video_opt.*] diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py new file mode 100644 index 0000000..49fa3b2 --- /dev/null +++ b/tests/layers/test_layer_impl.py @@ -0,0 +1,57 @@ +from typing import Tuple + +import pytest + +import bitorch +from bitorch import RuntimeMode +from bitorch.layers.layer_registry import LayerRegistry, _LayerImplementation + +_registry = LayerRegistry("TestLayer") + + +class TestLayerImplementation(_LayerImplementation): + def __init__(self, *args): + super().__init__(_registry, *args) + + +@TestLayerImplementation(RuntimeMode.DEFAULT) +class TestLayerDefault: + def __init__(self, s: str, val: int = 42) -> None: + self.s = s + self.val = val + + def class_name(self) -> str: + return self.__class__.__name__ + + +@TestLayerImplementation(RuntimeMode.TRAIN) +class TestLayerTrain: + def __init__(self, s: str, val: int = 42) -> None: + self.s = s + self.val = val + + def class_name(self) -> str: + return self.__class__.__name__ + + +TestLayer = TestLayerDefault + + +@pytest.fixture(scope='function', autouse=True) +def set_default_mode(): + bitorch.mode = RuntimeMode.DEFAULT + yield None + bitorch.mode = RuntimeMode.DEFAULT + + +def test_default_impl(): + s = TestLayer("Hello World", val=21) + assert s.val == 21 + assert s.class_name() == "TestLayerDefault" + + +def test_train_impl(): + bitorch.mode = RuntimeMode.TRAIN + s = TestLayer("Hello World", val=21) + assert s.val == 21 + assert s.class_name() == "TestLayerTrain" diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_mode.py b/tests/test_mode.py new file mode 100644 index 0000000..9ac8dd9 --- /dev/null +++ b/tests/test_mode.py @@ -0,0 +1,74 @@ +import pytest + +import bitorch +from bitorch import RuntimeMode + + +def test_mode_creation_from_name(): + for mode_str in RuntimeMode.__members__.keys(): + assert isinstance(RuntimeMode.from_string(mode_str), RuntimeMode) + + +def test_mode_supports_self(): + for mode in RuntimeMode.__members__.values(): + assert mode.is_supported_by(mode) + + +def test_mode_does_not_support_other_mode(): + for mode in RuntimeMode.__members__.values(): + for other_mode in RuntimeMode.__members__.values(): + if mode == other_mode: + continue + assert not mode.is_supported_by(other_mode) + + +def test_mode_self_addition(): + for mode in RuntimeMode.__members__.values(): + same_mode_twice = mode + mode + assert same_mode_twice == mode + + +def test_mode_addition_supports_both(): + for mode in RuntimeMode.__members__.values(): + for other_mode in RuntimeMode.__members__.values(): + if mode == other_mode: + continue + added_modes = mode + other_mode + assert mode.is_supported_by(added_modes) + assert other_mode.is_supported_by(added_modes) + + +def test_str_output(): + assert str(RuntimeMode.DEFAULT) == "default" + + +def test_repr_output(): + assert repr(RuntimeMode.DEFAULT) == "" + + +def test_bitorch_default_mode(): + assert bitorch.mode == RuntimeMode.DEFAULT + + +@pytest.fixture() +def set_train_mode(): + bitorch.mode = RuntimeMode.TRAIN + yield None + bitorch.mode = RuntimeMode.DEFAULT + + +def test_set_bitorch_mode(set_train_mode): + assert bitorch.mode == RuntimeMode.TRAIN + + +@pytest.fixture() +def reset_modes(): + bitorch.mode = RuntimeMode.DEFAULT + yield None + bitorch.mode = RuntimeMode.DEFAULT + + +def test_setting_different_modes(reset_modes): + assert bitorch.mode == RuntimeMode.DEFAULT + bitorch.mode = RuntimeMode.TRAIN + assert bitorch.mode == RuntimeMode.TRAIN From 32f3c2cdcd87eddb705a55a0072cd6cdaaaf669c Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 3 Jun 2022 18:09:34 +0200 Subject: [PATCH 016/208] fix switchable layer recursion problem --- README.md | 2 +- bitorch/__init__.py | 3 +- bitorch/layers/extensions/__init__.py | 0 .../layers/extensions/layer_implementation.py | 60 +++++++++++++++++++ bitorch/layers/extensions/switchable_layer.py | 34 +++++++++++ bitorch/layers/layer_registry.py | 51 ---------------- bitorch/layers/qlinear.py | 14 ++--- bitorch/models/base.py | 14 +++++ bitorch/quantizations/base.py | 2 +- bitorch/quantizations/dorefa.py | 6 +- bitorch/runtime_mode.py | 11 ++-- examples/mnist/train_mnist.py | 50 +++++++++------- requirements.txt | 4 +- tests/layers/test_layer_impl.py | 21 ++++--- tests/layers/test_switchable_layer.py | 47 +++++++++++++++ tests/test_mode.py | 12 ++-- 16 files changed, 221 insertions(+), 110 deletions(-) create mode 100644 bitorch/layers/extensions/__init__.py create mode 100644 bitorch/layers/extensions/layer_implementation.py create mode 100644 bitorch/layers/extensions/switchable_layer.py delete mode 100644 bitorch/layers/layer_registry.py create mode 100644 tests/layers/test_switchable_layer.py diff --git a/README.md b/README.md index 95e1d34..e34dd04 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Note, that you can also request a specific PyTorch version directly, e.g. for CU pip install bitorch --extra-index-url https://download.pytorch.org/whl/cu113 ``` -To use advanced logging capabilities, install the optional dependencies as well: +If you want to be able to run the examples, install the optional dependencies as well: ```bash pip install "bitorch[opt]" diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 17c8282..5becd31 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -5,8 +5,7 @@ from typing import List from .config import Config -from .runtime_mode import RuntimeMode - +from .runtime_mode import RuntimeMode, runtime_mode_type mode: RuntimeMode = RuntimeMode.DEFAULT diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py new file mode 100644 index 0000000..30caca8 --- /dev/null +++ b/bitorch/layers/extensions/layer_implementation.py @@ -0,0 +1,60 @@ +from abc import ABC +from typing import Any, Union, Set, Optional + +import bitorch +from bitorch import runtime_mode_type, RuntimeMode +from .switchable_layer import SwitchableLayer + + +class LayerImplementation(ABC): + def __init__(self, registry: "LayerRegistry", supports_modes: runtime_mode_type) -> None: + self.registry = registry + assert RuntimeMode.is_combined_mode(supports_modes), f"invalid mode {supports_modes} given" + self.supports_modes = supports_modes + self.__initialized = False + self.class_: Any = None + self.class_name = "" + + def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", SwitchableLayer]: + if not self.__initialized: + # this function is called once when @Decorator is used, we need to initialize this object correctly + self.__initialized = True + self.class_ = args[0] + self.class_name = self.class_.__name__ + self.registry.register(self) + return self + + # on later calls we need to provide the correct layer implementation + correct_layer_implementation = self.registry.get_layer() + if self == correct_layer_implementation: + # this class provides the correct implementation for the current mode (recursion stop) + return SwitchableLayer(self.class_, *args, **kwargs) + # return self.class_(*args, **kwargs) + # call this method again but on the correct base class + return correct_layer_implementation(*args, **kwargs) + + +class LayerRegistry: + def __init__(self, name: str) -> None: + self.name = name + self._class = None + self.registered_layers: Set[LayerImplementation] = set() + + def __contains__(self, item: Any) -> bool: + return item.__class__ in map(lambda x: x.class_, self.registered_layers) + + def register(self, layer: LayerImplementation) -> None: + self.registered_layers.add(layer) + + def get_layer(self, mode: Optional[RuntimeMode] = None) -> LayerImplementation: + if mode is None: + mode = bitorch.mode + available_layers = [] + for layer in self.registered_layers: + if mode.is_supported_by(layer.supports_modes): + available_layers.append(layer) + if len(available_layers) > 1: + RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") + if len(available_layers) == 0: + raise RuntimeError(f"No layer implementation for '{self.name}' available (mode='{mode}').") + return available_layers[0] diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/switchable_layer.py new file mode 100644 index 0000000..f62c3ac --- /dev/null +++ b/bitorch/layers/extensions/switchable_layer.py @@ -0,0 +1,34 @@ +from typing import Any + + +class SwitchableLayer: + def __init__(self, impl_class: Any, *args: Any, **kwargs: Any) -> None: + self._layer_implementation = impl_class(*args, **kwargs) + + def __getattr__(self, item: Any) -> Any: + if item == "_layer_implementation": + return self.__dict__[item] + attr_value = getattr(self._layer_implementation, item) + if attr_value == self._layer_implementation: + return self + if callable(attr_value): + def patch_function(*args, **kwargs): + fn_return_val = attr_value(*args, **kwargs) + if fn_return_val == self._layer_implementation: + return self + return fn_return_val + return patch_function + return attr_value + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self._layer_implementation(*args, **kwargs) + + def __setattr__(self, key: Any, value: Any) -> None: + if key == "_layer_implementation": + self.__dict__[key] = value + return + setattr(self._layer_implementation, key, value) + + @property # type: ignore[misc] + def __class__(self) -> Any: + return self._layer_implementation.__class__ diff --git a/bitorch/layers/layer_registry.py b/bitorch/layers/layer_registry.py deleted file mode 100644 index ac1442c..0000000 --- a/bitorch/layers/layer_registry.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Set, Optional, Any - -import bitorch -from .. import RuntimeMode -from ..runtime_mode import runtime_mode_type - - -class LayerRegistry: - def __init__(self, name: str) -> None: - self.name = name - self.registered_layers: Set[_LayerImplementation] = set() - - def register(self, layer: "_LayerImplementation") -> None: - self.registered_layers.add(layer) - - def get_layer(self, mode: Optional[RuntimeMode] = None) -> "_LayerImplementation": - if mode is None: - mode = bitorch.mode - available_layers = [] - for layer in self.registered_layers: - if mode.is_supported_by(layer.supports_modes): - available_layers.append(layer) - if len(available_layers) > 1: - RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") - if len(available_layers) == 0: - raise RuntimeError(f"No layer implementation for '{self.name}' available (mode='{mode}').") - return available_layers[0] - - -class _LayerImplementation: - def __init__(self, registry: LayerRegistry, supports_modes: runtime_mode_type) -> None: - self.registry = registry - self.supports_modes = supports_modes - assert self.supports_modes > 0, "Invalid mode given" - self.__initialized = False - self.class_: Any = None - self.class_name = "" - - def __call__(self, *args: Any, **kwargs: Any) -> Any: - if not self.__initialized: - self.__initialized = True - self.class_ = args[0] - self.class_name = self.class_.__name__ - self.registry.register(self) - return self - current_layer = self.registry.get_layer() - if self == current_layer: - # this class provides the correct implementation for the current mode (recursion stop) - return self.class_(*args, **kwargs) - # call this method again but on the correct base class - return current_layer(*args, **kwargs) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index d41c8d9..3cfb2aa 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -6,18 +6,19 @@ from torch.nn import Linear from torch.nn.functional import linear +from bitorch import RuntimeMode, runtime_mode_type +from bitorch.quantizations import Quantization from .config import config -from .layer_registry import LayerRegistry, _LayerImplementation +from .extensions.layer_implementation import LayerImplementation, LayerRegistry from .qactivation import QActivation -from ..quantizations import Quantization -from ..runtime_mode import RuntimeMode, runtime_mode_type -_registry = LayerRegistry("QLinear") +q_linear_registry = LayerRegistry("QLinear") -class QLinearImplementation(_LayerImplementation): + +class QLinearImplementation(LayerImplementation): def __init__(self, supports_modes: runtime_mode_type) -> None: - super().__init__(_registry, supports_modes) + super().__init__(q_linear_registry, supports_modes) @QLinearImplementation(RuntimeMode.DEFAULT) @@ -41,7 +42,6 @@ def __init__( function. Defaults to None. **kwargs (keyword Argument list): keyword arguments for linear layer """ - super().__init__(*args, **kwargs) # type: ignore self.weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index f3f4029..ff40675 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -4,8 +4,11 @@ import torch from torch import nn +import bitorch +from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct +from ..layers.qlinear import q_linear_registry, QLinear class Model(nn.Module): @@ -66,3 +69,14 @@ def initialize(self) -> None: nn.init.constant_(module.bias, 0) elif isinstance(module, nn.Linear): nn.init.xavier_normal_(module.weight) + + def convert(self, new_mode: RuntimeMode, device=None): + for module in self._model.modules(): + if module in q_linear_registry: + print("Converting:", module, "...") + bitorch.mode = new_mode + new_module = QLinear(module.in_features, module.out_features, device=device) + new_module.weight = module.weight + if hasattr(module, "bias"): + new_module.bias = module.bias + print(" ... to", new_module) diff --git a/bitorch/quantizations/base.py b/bitorch/quantizations/base.py index 8505a36..5db075e 100644 --- a/bitorch/quantizations/base.py +++ b/bitorch/quantizations/base.py @@ -45,7 +45,7 @@ class Quantization(nn.Module): """superclass for quantization modules""" name = "None" - bitwidth = -1 + bit_width = -1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """quantize the input tensor. It is recommended to use a torch.Function to also maniputlate backward behaiviour. See diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index e63c0a1..cab8212 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -16,7 +16,7 @@ class WeightDoReFa(Quantization): """ name = "weightdorefa" - bitwidth = config.dorefa_bits + bit_width = config.dorefa_bits def __init__(self, bits: Union[int, None] = None) -> None: """Initiates quantization bits. @@ -25,8 +25,8 @@ def __init__(self, bits: Union[int, None] = None) -> None: bits (int, optional): number of bits to quantize into. Defaults to None. """ super(WeightDoReFa, self).__init__() - self.bitwidth = bits or config.dorefa_bits - self._max_value = 2 ** self.bitwidth - 1 + self.bit_width = bits or config.dorefa_bits + self._max_value = 2 ** self.bit_width - 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """DoReFas the tensor to desired bit resolution using weight dorefa. diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index fcf42e8..6a3d9bc 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -3,15 +3,16 @@ from functools import total_ordering +__all__ = ["RuntimeMode", "runtime_mode_type"] + + runtime_mode_type = Union["RuntimeMode", int] @total_ordering class RuntimeMode(Enum): DEFAULT = 1 - TRAIN = 2 - CPP_INFERENCE = 4 - GPU_INFERENCE = 8 + INFERENCE_AUTO = 2 def __add__(self, other: runtime_mode_type) -> runtime_mode_type: if self._to_int(self) == self._to_int(other): @@ -53,9 +54,7 @@ def _to_int(mode: runtime_mode_type) -> int: def from_string(level: str) -> "RuntimeMode": return { "default": RuntimeMode.DEFAULT, - "train": RuntimeMode.TRAIN, - "cpp_inference": RuntimeMode.CPP_INFERENCE, - "gpu_inference": RuntimeMode.GPU_INFERENCE, + "inference_auto": RuntimeMode.INFERENCE_AUTO, }[level.lower()] @staticmethod diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index af5aa2e..8e67ccb 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -3,43 +3,49 @@ """ from __future__ import print_function + import argparse + import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.optim.lr_scheduler import StepLR -from bitorch import datasets as bitorch_datasets import bitorch.layers as qnn +from bitorch import datasets as bitorch_datasets, RuntimeMode +from bitorch.datasets import MNIST +from bitorch.models import Model + +from bitorch_inference_engine.layers.qlinear import QLinearInf -class QuantizedMLP(nn.Module): +class QuantizedMLP(Model): def __init__(self, num_hidden_units_1=128, num_hidden_units_2=64): - super().__init__() - self.flatten = nn.Flatten() - self.fc1 = qnn.QLinear(784, num_hidden_units_1) - self.act1 = nn.PReLU() - self.bn1 = nn.BatchNorm1d(num_hidden_units_1) + super().__init__(dataset=MNIST) + self._model.flatten = nn.Flatten() + self._model.fc1 = qnn.QLinear(784, num_hidden_units_1) + self._model.act1 = nn.PReLU() + self._model.bn1 = nn.BatchNorm1d(num_hidden_units_1) - self.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2) - self.act2 = nn.PReLU() - self.bn2 = nn.BatchNorm1d(num_hidden_units_2) + self._model.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2) + self._model.act2 = nn.PReLU() + self._model.bn2 = nn.BatchNorm1d(num_hidden_units_2) - self.fc3 = qnn.QLinear(num_hidden_units_2, 10) + self._model.fc3 = qnn.QLinear(num_hidden_units_2, 10) def forward(self, x): - x = self.flatten(x) + x = self._model.flatten(x) - x = self.fc1(x) - x = self.act1(x) - x = self.bn1(x) + x = self._model.fc1(x) + x = self._model.act1(x) + x = self._model.bn1(x) - x = self.fc2(x) - x = self.act2(x) - x = self.bn2(x) + x = self._model.fc2(x) + x = self._model.act2(x) + x = self._model.bn2(x) - x = self.fc3(x) + x = self._model.fc3(x) output = F.log_softmax(x, dim=1) return output @@ -137,7 +143,7 @@ def main(): torch.manual_seed(args.seed) - device = torch.device("cuda" if use_cuda else "cpu") + device = torch.device("cuda:0" if use_cuda else "cpu") train_kwargs = {'batch_size': args.batch_size} test_kwargs = {'batch_size': args.test_batch_size} @@ -155,6 +161,7 @@ def main(): if args.model == "mlp": model = QuantizedMLP().to(device) + print(model) else: model = Net().to(device) optimizer = optim.Adadelta(model.parameters(), lr=args.lr) @@ -165,6 +172,9 @@ def main(): test(model, device, test_loader) scheduler.step() + inference_model = model.convert(RuntimeMode.INFERENCE_AUTO, device=device) + test(inference_model, device, test_loader) + if args.save_model: torch.save(model.state_dict(), "mnist_cnn.pt") diff --git a/requirements.txt b/requirements.txt index 7b291b7..7334424 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -torch~=1.11.0 -torchvision~=0.12.0 +torch +torchvision bitorchinfo matplotlib numpy diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 49fa3b2..4e2af69 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -1,21 +1,19 @@ -from typing import Tuple - import pytest import bitorch from bitorch import RuntimeMode -from bitorch.layers.layer_registry import LayerRegistry, _LayerImplementation +from bitorch.layers.extensions.layer_implementation import LayerImplementation, LayerRegistry _registry = LayerRegistry("TestLayer") -class TestLayerImplementation(_LayerImplementation): +class TestLayerImplementation(LayerImplementation): def __init__(self, *args): super().__init__(_registry, *args) @TestLayerImplementation(RuntimeMode.DEFAULT) -class TestLayerDefault: +class TestLayerDefaultMode: def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val @@ -24,8 +22,8 @@ def class_name(self) -> str: return self.__class__.__name__ -@TestLayerImplementation(RuntimeMode.TRAIN) -class TestLayerTrain: +@TestLayerImplementation(RuntimeMode.INFERENCE_AUTO) +class TestLayerOtherMode: def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val @@ -34,7 +32,7 @@ def class_name(self) -> str: return self.__class__.__name__ -TestLayer = TestLayerDefault +TestLayer = TestLayerDefaultMode @pytest.fixture(scope='function', autouse=True) @@ -47,11 +45,12 @@ def set_default_mode(): def test_default_impl(): s = TestLayer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerDefault" + assert s.class_name() == "TestLayerDefaultMode" def test_train_impl(): - bitorch.mode = RuntimeMode.TRAIN + bitorch.mode = RuntimeMode.INFERENCE_AUTO s = TestLayer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerTrain" + assert s.class_name() == "TestLayerOtherMode" + diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py new file mode 100644 index 0000000..b795a74 --- /dev/null +++ b/tests/layers/test_switchable_layer.py @@ -0,0 +1,47 @@ +import pytest +import torch +from torch import nn + +from bitorch.layers.extensions.switchable_layer import SwitchableLayer + + +class Layer(nn.Module): + def __init__(self, x=10): + super().__init__() + self.x = x + + def foo(self): + assert isinstance(self.x, int) + return "foo" + + @property + def self_property(self): + return self + + def self_function(self): + return self + + +@pytest.mark.parametrize("test_wrapped_layer", [False, True]) +def test_switchable_layer(test_wrapped_layer): + if test_wrapped_layer: + layer = SwitchableLayer(Layer, 42) + else: + layer = Layer(42) + assert layer.x == 42 + layer.x = 3 + assert layer.x == 3 + assert layer.self_function() == layer + assert layer.self_property == layer + + def test_class_assertions(layer_): + assert isinstance(layer_, nn.Module) + assert isinstance(layer_, Layer) + assert test_wrapped_layer == isinstance(layer_, SwitchableLayer) + + test_class_assertions(layer) + + moved_layer = layer.to(torch.device("cpu")) + + test_class_assertions(moved_layer) + assert layer == moved_layer diff --git a/tests/test_mode.py b/tests/test_mode.py index 9ac8dd9..ce036ef 100644 --- a/tests/test_mode.py +++ b/tests/test_mode.py @@ -51,14 +51,14 @@ def test_bitorch_default_mode(): @pytest.fixture() -def set_train_mode(): - bitorch.mode = RuntimeMode.TRAIN +def set_inference_mode(): + bitorch.mode = RuntimeMode.INFERENCE_AUTO yield None bitorch.mode = RuntimeMode.DEFAULT -def test_set_bitorch_mode(set_train_mode): - assert bitorch.mode == RuntimeMode.TRAIN +def test_set_bitorch_mode(set_inference_mode): + assert bitorch.mode == RuntimeMode.INFERENCE_AUTO @pytest.fixture() @@ -70,5 +70,5 @@ def reset_modes(): def test_setting_different_modes(reset_modes): assert bitorch.mode == RuntimeMode.DEFAULT - bitorch.mode = RuntimeMode.TRAIN - assert bitorch.mode == RuntimeMode.TRAIN + bitorch.mode = RuntimeMode.INFERENCE_AUTO + assert bitorch.mode == RuntimeMode.INFERENCE_AUTO From d040a8318cffb2b32047655e1f1b71aa557e0a50 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 7 Jun 2022 13:32:57 +0200 Subject: [PATCH 017/208] changed layer implementation, layer conversion is based on LayerRecipe + LayerContainer --- bitorch/__init__.py | 19 ++++++++- .../layers/extensions/layer_implementation.py | 42 ++++++++++++++++--- bitorch/layers/extensions/switchable_layer.py | 5 ++- bitorch/layers/qlinear.py | 2 +- bitorch/models/base.py | 29 ++++++++----- examples/mnist/train_mnist.py | 4 +- tests/layers/test_switchable_layer.py | 6 +-- 7 files changed, 84 insertions(+), 23 deletions(-) diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 5becd31..555dbef 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -2,7 +2,7 @@ from argparse import ArgumentParser, Namespace from importlib import import_module from pathlib import Path -from typing import List +from typing import List, Type, Optional from .config import Config from .runtime_mode import RuntimeMode, runtime_mode_type @@ -10,6 +10,23 @@ mode: RuntimeMode = RuntimeMode.DEFAULT +class _ModeSetter(object): + def __init__(self, new_mode: RuntimeMode) -> None: + self._previous_mode = mode + self._new_mode = new_mode + + def __enter__(self) -> "_ModeSetter": + global mode + mode = self._new_mode + return self + + def __exit__(self, exc_type: Type[BaseException], exc_val: Optional[BaseException], exc_tb) -> None: + global mode + mode = self._previous_mode + + +change_mode = _ModeSetter + configs_by_name = {} current_dir = Path(__file__).resolve().parent diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 30caca8..e7909c6 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,12 +1,27 @@ from abc import ABC -from typing import Any, Union, Set, Optional +from dataclasses import dataclass +from typing import Any, Union, Set, Optional, Dict, Tuple import bitorch from bitorch import runtime_mode_type, RuntimeMode -from .switchable_layer import SwitchableLayer +from .switchable_layer import LayerContainer + + +@dataclass(eq=False, frozen=True) +class LayerRecipe: + """Class to store args and kwargs used to create a particular layer. Allows to create other versions later on.""" + # registry: "LayerRegistry" + container: "LayerContainer" + args: Tuple[Any] + kwargs: Dict[str, Any] class LayerImplementation(ABC): + """ + Superclass for storing different implementations of a common layer. + + It registers all decorated classes in the given registry and + """ def __init__(self, registry: "LayerRegistry", supports_modes: runtime_mode_type) -> None: self.registry = registry assert RuntimeMode.is_combined_mode(supports_modes), f"invalid mode {supports_modes} given" @@ -15,7 +30,7 @@ def __init__(self, registry: "LayerRegistry", supports_modes: runtime_mode_type) self.class_: Any = None self.class_name = "" - def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", SwitchableLayer]: + def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", LayerContainer]: if not self.__initialized: # this function is called once when @Decorator is used, we need to initialize this object correctly self.__initialized = True @@ -28,8 +43,12 @@ def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", Sw correct_layer_implementation = self.registry.get_layer() if self == correct_layer_implementation: # this class provides the correct implementation for the current mode (recursion stop) - return SwitchableLayer(self.class_, *args, **kwargs) - # return self.class_(*args, **kwargs) + if self.registry.is_replacing: + return self.class_(*args, **kwargs) + else: + layer_container = LayerContainer(self.class_, *args, **kwargs) + self.registry.add_recipe(LayerRecipe(container=layer_container, args=args, kwargs=kwargs)) + return layer_container # call this method again but on the correct base class return correct_layer_implementation(*args, **kwargs) @@ -39,6 +58,19 @@ def __init__(self, name: str) -> None: self.name = name self._class = None self.registered_layers: Set[LayerImplementation] = set() + self.instance_recipes: Set[LayerRecipe] = set() + self.is_replacing = False + + def get_replacement(self, *args: Any, **kwargs: Any) -> Any: + self.is_replacing = True + replacement_layer = self.get_layer()(*args, **kwargs) + self.is_replacing = False + return replacement_layer + + def add_recipe(self, new_recipe: LayerRecipe) -> None: + if self.is_replacing: + return + self.instance_recipes.add(new_recipe) def __contains__(self, item: Any) -> bool: return item.__class__ in map(lambda x: x.class_, self.registered_layers) diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/switchable_layer.py index f62c3ac..f91cdad 100644 --- a/bitorch/layers/extensions/switchable_layer.py +++ b/bitorch/layers/extensions/switchable_layer.py @@ -1,10 +1,13 @@ from typing import Any -class SwitchableLayer: +class LayerContainer: def __init__(self, impl_class: Any, *args: Any, **kwargs: Any) -> None: self._layer_implementation = impl_class(*args, **kwargs) + def replace_layer_implementation(self, new_implementation: Any) -> None: + self._layer_implementation = new_implementation + def __getattr__(self, item: Any) -> Any: if item == "_layer_implementation": return self.__dict__[item] diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 3cfb2aa..0c86f33 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -17,7 +17,7 @@ class QLinearImplementation(LayerImplementation): - def __init__(self, supports_modes: runtime_mode_type) -> None: + def __init__(self, supports_modes: runtime_mode_type, compatibility_function: Optional[Callable] = None) -> None: super().__init__(q_linear_registry, supports_modes) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index ff40675..ca84b02 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -8,6 +8,7 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct +from ..layers.extensions.switchable_layer import LayerContainer from ..layers.qlinear import q_linear_registry, QLinear @@ -70,13 +71,21 @@ def initialize(self) -> None: elif isinstance(module, nn.Linear): nn.init.xavier_normal_(module.weight) - def convert(self, new_mode: RuntimeMode, device=None): - for module in self._model.modules(): - if module in q_linear_registry: - print("Converting:", module, "...") - bitorch.mode = new_mode - new_module = QLinear(module.in_features, module.out_features, device=device) - new_module.weight = module.weight - if hasattr(module, "bias"): - new_module.bias = module.bias - print(" ... to", new_module) + def convert(self, new_mode: RuntimeMode, device=None, verbose=False): + with bitorch.change_mode(new_mode): + for registry in (q_linear_registry, ): + for recipe in registry.instance_recipes: + module = recipe.container + assert isinstance(module, LayerContainer) + if verbose: + print("Converting", module) + new_kwargs = {} + new_kwargs.update(recipe.kwargs) + new_kwargs["device"] = device + replacement_module = registry.get_replacement(*recipe.args, **new_kwargs) + if hasattr(module, "weight"): + replacement_module.weight = module.weight + if hasattr(module, "bias"): + replacement_module.bias = module.bias + module.replace_layer_implementation(replacement_module) + return self diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 8e67ccb..fb68177 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -24,7 +24,7 @@ class QuantizedMLP(Model): def __init__(self, num_hidden_units_1=128, num_hidden_units_2=64): super().__init__(dataset=MNIST) self._model.flatten = nn.Flatten() - self._model.fc1 = qnn.QLinear(784, num_hidden_units_1) + self._model.fc1 = nn.Linear(784, num_hidden_units_1) self._model.act1 = nn.PReLU() self._model.bn1 = nn.BatchNorm1d(num_hidden_units_1) @@ -172,7 +172,7 @@ def main(): test(model, device, test_loader) scheduler.step() - inference_model = model.convert(RuntimeMode.INFERENCE_AUTO, device=device) + inference_model = model.convert(RuntimeMode.INFERENCE_AUTO, device=device, verbose=True) test(inference_model, device, test_loader) if args.save_model: diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index b795a74..31485ec 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -2,7 +2,7 @@ import torch from torch import nn -from bitorch.layers.extensions.switchable_layer import SwitchableLayer +from bitorch.layers.extensions.switchable_layer import LayerContainer class Layer(nn.Module): @@ -25,7 +25,7 @@ def self_function(self): @pytest.mark.parametrize("test_wrapped_layer", [False, True]) def test_switchable_layer(test_wrapped_layer): if test_wrapped_layer: - layer = SwitchableLayer(Layer, 42) + layer = LayerContainer(Layer, 42) else: layer = Layer(42) assert layer.x == 42 @@ -37,7 +37,7 @@ def test_switchable_layer(test_wrapped_layer): def test_class_assertions(layer_): assert isinstance(layer_, nn.Module) assert isinstance(layer_, Layer) - assert test_wrapped_layer == isinstance(layer_, SwitchableLayer) + assert test_wrapped_layer == isinstance(layer_, LayerContainer) test_class_assertions(layer) From d6a49b6b488b6131f15dd5aaa6afdec4871b2083 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 7 Jun 2022 14:22:10 +0200 Subject: [PATCH 018/208] fix error --- bitorch/layers/qlinear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 0c86f33..3cfb2aa 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -17,7 +17,7 @@ class QLinearImplementation(LayerImplementation): - def __init__(self, supports_modes: runtime_mode_type, compatibility_function: Optional[Callable] = None) -> None: + def __init__(self, supports_modes: runtime_mode_type) -> None: super().__init__(q_linear_registry, supports_modes) From 111749ad4796ee02d3a606a7be10f7219b2868a4 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 7 Jun 2022 16:53:25 +0200 Subject: [PATCH 019/208] fix type --- bitorch/__init__.py | 8 +++++- .../layers/extensions/layer_implementation.py | 2 +- bitorch/layers/extensions/switchable_layer.py | 27 ++++++++++++++----- bitorch/models/base.py | 2 +- tests/layers/test_switchable_layer.py | 12 ++++++--- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 555dbef..4a18b41 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser, Namespace from importlib import import_module from pathlib import Path +from types import TracebackType from typing import List, Type, Optional from .config import Config @@ -20,7 +21,12 @@ def __enter__(self) -> "_ModeSetter": mode = self._new_mode return self - def __exit__(self, exc_type: Type[BaseException], exc_val: Optional[BaseException], exc_tb) -> None: + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType] + ) -> None: global mode mode = self._previous_mode diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index e7909c6..e5e6b4f 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -12,7 +12,7 @@ class LayerRecipe: """Class to store args and kwargs used to create a particular layer. Allows to create other versions later on.""" # registry: "LayerRegistry" container: "LayerContainer" - args: Tuple[Any] + args: Tuple[Any, ...] kwargs: Dict[str, Any] diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/switchable_layer.py index f91cdad..589824b 100644 --- a/bitorch/layers/extensions/switchable_layer.py +++ b/bitorch/layers/extensions/switchable_layer.py @@ -15,12 +15,27 @@ def __getattr__(self, item: Any) -> Any: if attr_value == self._layer_implementation: return self if callable(attr_value): - def patch_function(*args, **kwargs): - fn_return_val = attr_value(*args, **kwargs) - if fn_return_val == self._layer_implementation: - return self - return fn_return_val - return patch_function + # dirty patch functions and classes + # they should return this LayerContainer instead of themselves + # required for e.g. pytorch's .to(device) function + other = self + + class Patch: + def __call__(self, *args: Any, **kwargs: Any) -> Any: + fn_return_val = attr_value(*args, **kwargs) + if fn_return_val == other._layer_implementation: + return other + return fn_return_val + + def __getattr__(self, item_: Any) -> Any: + return getattr(attr_value, item_) + + # needed for tests: + @property # type: ignore[misc] + def __class__(self) -> Any: + return attr_value.__class__ + + return Patch() return attr_value def __call__(self, *args: Any, **kwargs: Any) -> Any: diff --git a/bitorch/models/base.py b/bitorch/models/base.py index ca84b02..6c6ce4d 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -71,7 +71,7 @@ def initialize(self) -> None: elif isinstance(module, nn.Linear): nn.init.xavier_normal_(module.weight) - def convert(self, new_mode: RuntimeMode, device=None, verbose=False): + def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": with bitorch.change_mode(new_mode): for registry in (q_linear_registry, ): for recipe in registry.instance_recipes: diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index 31485ec..5e9c4f9 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -5,14 +5,18 @@ from bitorch.layers.extensions.switchable_layer import LayerContainer +class Foo: + pass + + class Layer(nn.Module): def __init__(self, x=10): super().__init__() self.x = x + self.foo = Foo() - def foo(self): - assert isinstance(self.x, int) - return "foo" + def get_foo(self): + return self.foo @property def self_property(self): @@ -37,6 +41,8 @@ def test_switchable_layer(test_wrapped_layer): def test_class_assertions(layer_): assert isinstance(layer_, nn.Module) assert isinstance(layer_, Layer) + assert isinstance(layer_.foo, Foo) + assert isinstance(layer_.get_foo(), Foo) assert test_wrapped_layer == isinstance(layer_, LayerContainer) test_class_assertions(layer) From ae576e5ee0cdaa67a0f08b39c9635949f31181cb Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 8 Jun 2022 17:08:56 +0200 Subject: [PATCH 020/208] bump mypy version --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 66e7f4f..c620948 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ build flake8 -mypy +mypy~=0.920 pep8-naming pytest sphinx From 1e00ff3a11426dd13d3424ceb03b6a1740124afa Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 8 Jun 2022 17:10:08 +0200 Subject: [PATCH 021/208] refactor implementation of layer extensions, add lots of tests, activate type hinting from signature, modify mnist example --- bitorch/__init__.py | 36 +-- bitorch/datasets/__init__.py | 2 +- bitorch/layers/__init__.py | 7 +- .../layers/extensions/layer_implementation.py | 228 +++++++++++++++--- bitorch/layers/extensions/switchable_layer.py | 3 + bitorch/layers/qlinear.py | 49 ++-- bitorch/models/base.py | 23 +- bitorch/runtime_mode.py | 56 ++++- docs/source/conf.py | 3 + examples/mnist/README.md | 9 + examples/mnist/train_mnist.py | 11 +- tests/layers/test_layer_impl.py | 83 ++++++- tests/models/test_model_conversion.py | 94 ++++++++ tests/{test_mode.py => test_runtime_mode.py} | 27 ++- 14 files changed, 497 insertions(+), 134 deletions(-) create mode 100644 examples/mnist/README.md create mode 100644 tests/models/test_model_conversion.py rename tests/{test_mode.py => test_runtime_mode.py} (74%) diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 4a18b41..05e1c50 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -2,37 +2,13 @@ from argparse import ArgumentParser, Namespace from importlib import import_module from pathlib import Path -from types import TracebackType -from typing import List, Type, Optional +from typing import List from .config import Config -from .runtime_mode import RuntimeMode, runtime_mode_type +from .runtime_mode import RuntimeMode, runtime_mode_type, change_mode, pause_wrapping mode: RuntimeMode = RuntimeMode.DEFAULT - -class _ModeSetter(object): - def __init__(self, new_mode: RuntimeMode) -> None: - self._previous_mode = mode - self._new_mode = new_mode - - def __enter__(self) -> "_ModeSetter": - global mode - mode = self._new_mode - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType] - ) -> None: - global mode - mode = self._previous_mode - - -change_mode = _ModeSetter - configs_by_name = {} current_dir = Path(__file__).resolve().parent @@ -90,8 +66,8 @@ def add_config_args(parser: ArgumentParser) -> None: Args: parser (ArgumentParser): parser to add the arguments to """ - for config in configs_by_name.values(): - config.add_config_arguments(parser) + for config_ in configs_by_name.values(): + config_.add_config_arguments(parser) def apply_args_to_configuration(args: Namespace) -> None: @@ -100,5 +76,5 @@ def apply_args_to_configuration(args: Namespace) -> None: Args: args (Namespace): the cli configurations """ - for config in configs_by_name.values(): - config.apply_args_to_configuration(args) + for config_ in configs_by_name.values(): + config_.apply_args_to_configuration(args) diff --git a/bitorch/datasets/__init__.py b/bitorch/datasets/__init__.py index 972707e..b870bd7 100644 --- a/bitorch/datasets/__init__.py +++ b/bitorch/datasets/__init__.py @@ -25,7 +25,7 @@ def dataset_from_name(name: str) -> Type[BasicDataset]: name-attribute) Args: - name (str): name of the dataset + name: name of the dataset Raises: ValueError: raised if no dataset under that name was found diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 67af28e..dd7e809 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -15,12 +15,15 @@ from .qconv1d import QConv1d, QConv1d_NoAct from .qconv2d import QConv2d, QConv2d_NoAct from .qconv3d import QConv3d, QConv3d_NoAct -from .qlinear import QLinear +from .qlinear import QLinear, QLinearBase from .pact import Pact from .qembedding import QEmbedding, QEmbeddingBag +from .extensions.layer_implementation import CustomImplementation + __all__ = [ "InputGraphicalDebug", "InputPrintDebug", "WeightGraphicalDebug", "WeightPrintDebug", "ShapePrintDebug", "QActivation", "QConv1d", "QConv2d", "QConv3d", "QConv1d_NoAct", - "QConv2d_NoAct", "QConv3d_NoAct", "QLinear", "QEmbedding", "QEmbeddingBag", "Pact", + "QConv2d_NoAct", "QConv3d_NoAct", "QLinear", "QLinearBase", "QEmbedding", "QEmbeddingBag", "Pact", + "CustomImplementation" ] diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index e5e6b4f..3b96537 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,6 +1,9 @@ from abc import ABC from dataclasses import dataclass -from typing import Any, Union, Set, Optional, Dict, Tuple +from typing import Any, Union, Set, Optional, Dict, Tuple, Type + +import torch +from torch import nn import bitorch from bitorch import runtime_mode_type, RuntimeMode @@ -9,84 +12,233 @@ @dataclass(eq=False, frozen=True) class LayerRecipe: - """Class to store args and kwargs used to create a particular layer. Allows to create other versions later on.""" - # registry: "LayerRegistry" - container: "LayerContainer" + """ + Data class to store a layer object and the arguments used to create it. + It allows to create other implementations of the same layer later on. + """ + layer: "LayerContainer" args: Tuple[Any, ...] kwargs: Dict[str, Any] +class BaseImplementation: + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + """Defines the class interface of a custom layer implementation of a certain layer type.""" + @classmethod + def is_default_implementation(cls) -> bool: + """ + Returns: + bool: whether this implementation is the default implementation of the current layer type + """ + raise NotImplementedError("Should be implemented by subclass.") + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + """ + Returns whether this layer class supports the implementation of a given layer recipe. + + Args: + recipe (LayerRecipe): the layer which should be checked for cloning + + Returns: + bool: Whether the layer can be cloned or not + """ + raise NotImplementedError("A custom layer should implement their own compatibility check.") + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + """ + Create a new layer based on a given layer recipe (can be expected to be from the default category). + + Args: + recipe (LayerRecipe): the layer which should be cloned + + Returns: + A clone of the LayerRecipe in the current class implementation + """ + raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") + + +class DefaultImplementation(BaseImplementation, ABC): + """Defines the class interface of a default layer implementation of a certain layer type.""" + @classmethod + def is_default_implementation(cls) -> bool: + return True + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + return True + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + return cls(*recipe.args, **recipe.kwargs) + + +class CustomImplementation(BaseImplementation, ABC): + """Defines the class interface of a custom layer implementation of a certain layer type.""" + @classmethod + def is_default_implementation(cls) -> bool: + return False + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + raise NotImplementedError("A custom layer should implement their own compatibility check.") + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") + + class LayerImplementation(ABC): """ - Superclass for storing different implementations of a common layer. + Superclass for storing different implementations of a common layer type. - It registers all decorated classes in the given registry and + It registers all decorated classes in the given registry. On creation of a decorated class, it + wraps the created class object in a layer container and stores the arguments used to create the layer. """ - def __init__(self, registry: "LayerRegistry", supports_modes: runtime_mode_type) -> None: + + registry: "LayerRegistry" + class_: Type[BaseImplementation] + class_name: str + _supported_modes: runtime_mode_type + __initialized: bool + + def __init__(self, registry: "LayerRegistry", supported_modes: runtime_mode_type) -> None: + """ + Define an implementation decorator for a certain type of layer. All implementations and objects of this type of + layer are stored in the given registry. + + Args: + registry: the registry which should store the implementation and objects of this layer type + supported_modes: the mode supported by the registering implementation + """ self.registry = registry - assert RuntimeMode.is_combined_mode(supports_modes), f"invalid mode {supports_modes} given" - self.supports_modes = supports_modes + assert RuntimeMode.is_combined_mode(supported_modes), f"invalid mode {supported_modes} given" + self._supported_modes = supported_modes self.__initialized = False - self.class_: Any = None + self.class_ = None # type: ignore self.class_name = "" - def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", LayerContainer]: + def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", LayerContainer, nn.Module]: if not self.__initialized: - # this function is called once when @Decorator is used, we need to initialize this object correctly - self.__initialized = True - self.class_ = args[0] - self.class_name = self.class_.__name__ - self.registry.register(self) - return self + # this object is called once when @Decorator is used, we need to initialize + return self._initialize(*args, **kwargs) + + if bitorch.mode == RuntimeMode.RAW: + return self.class_(*args, **kwargs) # type: ignore # on later calls we need to provide the correct layer implementation + return self._provide_layer_implementation(*args, **kwargs) + + def _initialize(self, class_: Type[BaseImplementation]) -> "LayerImplementation": + self.__initialized = True + self.class_ = class_ + self.class_name = self.class_.__name__ + if self._supported_modes == RuntimeMode.DEFAULT: + assert issubclass(self.class_, DefaultImplementation), \ + f"{self.class_name} should be a subclass of DefaultLayerImplementation." + else: + assert issubclass(self.class_, CustomImplementation), \ + f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " \ + f"implement the corresponding class methods)." + self.registry.register(self) + return self + + def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerContainer: correct_layer_implementation = self.registry.get_layer() if self == correct_layer_implementation: # this class provides the correct implementation for the current mode (recursion stop) - if self.registry.is_replacing: - return self.class_(*args, **kwargs) - else: - layer_container = LayerContainer(self.class_, *args, **kwargs) - self.registry.add_recipe(LayerRecipe(container=layer_container, args=args, kwargs=kwargs)) - return layer_container + layer_container = LayerContainer(self.class_, *args, **kwargs) + self.registry.add_recipe(LayerRecipe(layer=layer_container, args=args, kwargs=kwargs)) + return layer_container # call this method again but on the correct base class - return correct_layer_implementation(*args, **kwargs) + return correct_layer_implementation._provide_layer_implementation(*args, **kwargs) + + def supports_mode(self, mode: RuntimeMode) -> bool: + return mode.is_supported_by(self._supported_modes) + + def can_create_clone_from(self, recipe: LayerRecipe) -> bool: + return self.class_.can_clone(recipe) + + def get_replacement(self, recipe: LayerRecipe) -> Any: + return self.class_.create_clone_from(recipe) + + def is_default(self) -> bool: + return self.class_.is_default_implementation() class LayerRegistry: + """ + Stores all available implementations (and their supported modes) for a certain type of layer. + It also wraps these implementations and stores references to them, so they can be replaced easily. + Needs to be subclassed for each type of layer. + """ def __init__(self, name: str) -> None: self.name = name self._class = None - self.registered_layers: Set[LayerImplementation] = set() - self.instance_recipes: Set[LayerRecipe] = set() + self.layer_implementations: Set[LayerImplementation] = set() + self._instance_recipes: Set[LayerRecipe] = set() self.is_replacing = False - def get_replacement(self, *args: Any, **kwargs: Any) -> Any: - self.is_replacing = True - replacement_layer = self.get_layer()(*args, **kwargs) - self.is_replacing = False - return replacement_layer + @property + def layer_instances(self) -> Set["LayerContainer"]: + return set(x.layer for x in self._instance_recipes) + + def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: + if layer not in map(lambda x: x.layer, self._instance_recipes): + return None + return next(filter(lambda x: x.layer == layer, self._instance_recipes)) + + def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe) -> Any: + layer = self.get_layer(mode, recipe) + return layer.get_replacement(recipe) def add_recipe(self, new_recipe: LayerRecipe) -> None: if self.is_replacing: return - self.instance_recipes.add(new_recipe) + self._instance_recipes.add(new_recipe) def __contains__(self, item: Any) -> bool: - return item.__class__ in map(lambda x: x.class_, self.registered_layers) + return item.__class__ in map(lambda x: x.class_, self.layer_implementations) def register(self, layer: LayerImplementation) -> None: - self.registered_layers.add(layer) + self.layer_implementations.add(layer) - def get_layer(self, mode: Optional[RuntimeMode] = None) -> LayerImplementation: + def get_layer( + self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None + ) -> LayerImplementation: if mode is None: mode = bitorch.mode available_layers = [] - for layer in self.registered_layers: - if mode.is_supported_by(layer.supports_modes): - available_layers.append(layer) + for implementation in self.layer_implementations: + if not implementation.supports_mode(mode): + continue + if recipe and not implementation.can_create_clone_from(recipe): + continue + available_layers.append(implementation) if len(available_layers) > 1: RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") if len(available_layers) == 0: raise RuntimeError(f"No layer implementation for '{self.name}' available (mode='{mode}').") return available_layers[0] + + def clear(self) -> None: + while len(self._instance_recipes) > 0: + self._instance_recipes.pop() + + def unregister_custom_implementations(self) -> None: + to_remove = list(filter(lambda x: not x.is_default(), self.layer_implementations)) + for i in to_remove: + self.layer_implementations.remove(i) + + def convert_layers_to(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> None: + for recipe in self._instance_recipes: + module = recipe.layer + assert isinstance(module, LayerContainer) + if verbose: + print("Converting", module) + replacement_module = self.get_replacement(new_mode, recipe) + replacement_module.to(device) + module.replace_layer_implementation(replacement_module) diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/switchable_layer.py index 589824b..b672694 100644 --- a/bitorch/layers/extensions/switchable_layer.py +++ b/bitorch/layers/extensions/switchable_layer.py @@ -38,6 +38,9 @@ def __class__(self) -> Any: return Patch() return attr_value + def __repr__(self) -> "str": + return f"LayerContainer (at {hex(id(self))}), contains: {self._layer_implementation}" + def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._layer_implementation(*args, **kwargs) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 3cfb2aa..75cf56e 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -1,5 +1,4 @@ -"""Module containting the quantized linear layer""" - +"""Module containing the quantized linear layer""" from typing import Union import torch @@ -9,20 +8,11 @@ from bitorch import RuntimeMode, runtime_mode_type from bitorch.quantizations import Quantization from .config import config -from .extensions.layer_implementation import LayerImplementation, LayerRegistry +from .extensions.layer_implementation import LayerImplementation, LayerRegistry, DefaultImplementation from .qactivation import QActivation -q_linear_registry = LayerRegistry("QLinear") - - -class QLinearImplementation(LayerImplementation): - def __init__(self, supports_modes: runtime_mode_type) -> None: - super().__init__(q_linear_registry, supports_modes) - - -@QLinearImplementation(RuntimeMode.DEFAULT) -class QLinearDefault(Linear): +class QLinearBase(Linear): def __init__( self, *args: int, @@ -30,17 +20,17 @@ def __init__( gradient_cancellation_threshold: Union[float, None] = None, weight_quantization: Union[str, Quantization] = None, **kwargs: bool) -> None: - """Applys the given quantization functions on weights and inputs before applying the linear operation. + """Applies the given quantization functions on weights and inputs before applying the linear operation. Args: - *args (Argument list): positional arguments for linear layer + *args: positional arguments for linear layer input_quantization (Union[str, Quantization], optional): quantization module used for input quantization. Defaults to None. gradient_cancellation_threshold (Union[float, None], optional): threshold for input gradient cancellation. disabled if threshold is None. Defaults to None. weight_quantization (Union[str, Quantization], optional): quantization module or name of quantization function. Defaults to None. - **kwargs (keyword Argument list): keyword arguments for linear layer + **kwargs: keyword arguments for linear layer """ super().__init__(*args, **kwargs) # type: ignore self.weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) @@ -59,4 +49,29 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return linear(self.activation(x), self.weight_quantize(self.weight), self.bias) -QLinear = QLinearDefault +q_linear_registry = LayerRegistry("QLinear") + + +class QLinearImplementation(LayerImplementation): + """ + Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_linear_registry, supports_modes) + + +@QLinearImplementation(RuntimeMode.DEFAULT) +class QLinearDefaultImplementation(DefaultImplementation, QLinearBase): + """ + This class defines the default implementation of a QLinear layer (which is actually implemented by QLinearBase). + + To implement a custom QLinear implementation use QLinearBase as a super class instead. + """ + pass + + +QLinear = QLinearDefaultImplementation diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 6c6ce4d..1fb6dbb 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -4,12 +4,10 @@ import torch from torch import nn -import bitorch from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct -from ..layers.extensions.switchable_layer import LayerContainer -from ..layers.qlinear import q_linear_registry, QLinear +from bitorch.layers.qlinear import q_linear_registry class Model(nn.Module): @@ -72,20 +70,7 @@ def initialize(self) -> None: nn.init.xavier_normal_(module.weight) def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": - with bitorch.change_mode(new_mode): - for registry in (q_linear_registry, ): - for recipe in registry.instance_recipes: - module = recipe.container - assert isinstance(module, LayerContainer) - if verbose: - print("Converting", module) - new_kwargs = {} - new_kwargs.update(recipe.kwargs) - new_kwargs["device"] = device - replacement_module = registry.get_replacement(*recipe.args, **new_kwargs) - if hasattr(module, "weight"): - replacement_module.weight = module.weight - if hasattr(module, "bias"): - replacement_module.bias = module.bias - module.replace_layer_implementation(replacement_module) + for registry in (q_linear_registry, ): + registry.convert_layers_to(new_mode, device, verbose) + self._model.to(device) return self diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index 6a3d9bc..6988a52 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -1,16 +1,18 @@ from enum import Enum -from typing import Union, Any from functools import total_ordering +from types import TracebackType +from typing import Union, Any, Optional, Type +import bitorch -__all__ = ["RuntimeMode", "runtime_mode_type"] - +__all__ = ["RuntimeMode", "runtime_mode_type", "change_mode", "pause_wrapping"] runtime_mode_type = Union["RuntimeMode", int] @total_ordering class RuntimeMode(Enum): + RAW = 0 DEFAULT = 1 INFERENCE_AUTO = 2 @@ -29,7 +31,7 @@ def is_single_mode(mode: runtime_mode_type) -> bool: @staticmethod def is_combined_mode(mode: runtime_mode_type) -> bool: - return 0 < mode < RuntimeMode._max_val() + return 0 <= mode < RuntimeMode._max_val() def __lt__(self, other: Any) -> bool: if not isinstance(other, RuntimeMode) and not isinstance(other, int): @@ -53,13 +55,59 @@ def _to_int(mode: runtime_mode_type) -> int: @staticmethod def from_string(level: str) -> "RuntimeMode": return { + "raw": RuntimeMode.RAW, "default": RuntimeMode.DEFAULT, "inference_auto": RuntimeMode.INFERENCE_AUTO, }[level.lower()] @staticmethod def mode_compatible(required_mode: "RuntimeMode", provided_modes: runtime_mode_type) -> bool: + if required_mode == RuntimeMode.RAW.value or provided_modes == RuntimeMode.RAW.value: + return True return bool(RuntimeMode._to_int(required_mode) & RuntimeMode._to_int(provided_modes)) def is_supported_by(self, provided_modes: runtime_mode_type) -> bool: + if self._to_int(self) == RuntimeMode.RAW.value: + return True return self.mode_compatible(self, provided_modes) + + +class _PauseWrapping: + def __init__(self) -> None: + self._previous_mode = bitorch.mode + + def __enter__(self) -> "_PauseWrapping": + bitorch.mode = RuntimeMode.RAW + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType] + ) -> None: + bitorch.mode = self._previous_mode + + +class _SafeModeChanger: + def __init__(self, new_mode: RuntimeMode) -> None: + assert new_mode.is_single_mode(new_mode) + self._previous_mode = bitorch.mode + self._new_mode = new_mode + + def __enter__(self) -> "_SafeModeChanger": + bitorch.mode = self._new_mode + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType] + ) -> None: + bitorch.mode = self._previous_mode + + +change_mode = _SafeModeChanger + +pause_wrapping = _PauseWrapping diff --git a/docs/source/conf.py b/docs/source/conf.py index 2691121..75d96d5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -44,6 +44,9 @@ 'nbsphinx_link', ] +# Generate type hints +autodoc_typehints = "description" + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/examples/mnist/README.md b/examples/mnist/README.md new file mode 100644 index 0000000..bf82356 --- /dev/null +++ b/examples/mnist/README.md @@ -0,0 +1,9 @@ +# Example for MNIST + +In this example script we train a simple model for the MNIST dataset and also use the inference engine for speed up. + +For example, run the following to train an MLP with 3 layers (one of which is a binary layer), +or add `--help` for more arguments: +```bash +python train_mnist.py --epochs 10 --model mlp --log-interval 100 +``` diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index fb68177..31bae82 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -2,8 +2,6 @@ Adapted from official PyTorch example: https://github.com/pytorch/examples/blob/main/mnist/main.py """ -from __future__ import print_function - import argparse import torch @@ -16,23 +14,22 @@ from bitorch import datasets as bitorch_datasets, RuntimeMode from bitorch.datasets import MNIST from bitorch.models import Model - -from bitorch_inference_engine.layers.qlinear import QLinearInf +import bitorch_inference_engine class QuantizedMLP(Model): - def __init__(self, num_hidden_units_1=128, num_hidden_units_2=64): + def __init__(self, num_hidden_units_1=256, num_hidden_units_2=128): super().__init__(dataset=MNIST) self._model.flatten = nn.Flatten() self._model.fc1 = nn.Linear(784, num_hidden_units_1) self._model.act1 = nn.PReLU() self._model.bn1 = nn.BatchNorm1d(num_hidden_units_1) - self._model.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2) + self._model.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2, bias=False) self._model.act2 = nn.PReLU() self._model.bn2 = nn.BatchNorm1d(num_hidden_units_2) - self._model.fc3 = qnn.QLinear(num_hidden_units_2, 10) + self._model.fc3 = nn.Linear(num_hidden_units_2, 10) def forward(self, x): x = self._model.flatten(x) diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 4e2af69..bc00025 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -1,33 +1,55 @@ +from typing import Any + import pytest import bitorch from bitorch import RuntimeMode -from bitorch.layers.extensions.layer_implementation import LayerImplementation, LayerRegistry +from bitorch.layers.extensions.layer_implementation import LayerImplementation, LayerRegistry, \ + CustomImplementation, DefaultImplementation, LayerRecipe +from bitorch.layers.extensions.switchable_layer import LayerContainer + +TEST_MODE = RuntimeMode.INFERENCE_AUTO -_registry = LayerRegistry("TestLayer") +test_registry = LayerRegistry("TestLayer") class TestLayerImplementation(LayerImplementation): def __init__(self, *args): - super().__init__(_registry, *args) + super().__init__(test_registry, *args) @TestLayerImplementation(RuntimeMode.DEFAULT) -class TestLayerDefaultMode: +class TestLayerDefaultMode(DefaultImplementation): def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val + def do_something(self): + return f"{self.s}: {self.val} - made by {self.class_name()}" + def class_name(self) -> str: return self.__class__.__name__ -@TestLayerImplementation(RuntimeMode.INFERENCE_AUTO) -class TestLayerOtherMode: +@TestLayerImplementation(TEST_MODE) +class TestLayerCustomMode(CustomImplementation): def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + # assume this test class can only clone layers with 'vals' lower than 100 + val = recipe.kwargs.get("val", recipe.args[2] if 2 < len(recipe.args) else None) + return val < 100 + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + return cls(recipe.layer.s, recipe.layer.val) + + def do_something(self): + return f"{self.s}: {self.val} - made by {self.class_name()}" + def class_name(self) -> str: return self.__class__.__name__ @@ -36,21 +58,64 @@ def class_name(self) -> str: @pytest.fixture(scope='function', autouse=True) -def set_default_mode(): +def clean_environment(): + test_registry.clear() bitorch.mode = RuntimeMode.DEFAULT yield None + test_registry.clear() bitorch.mode = RuntimeMode.DEFAULT +def test_recipe(): + s1 = TestLayer("Hello World", val=21) + s2 = TestLayer("Hello World", 21) + + s1_recipe = test_registry.get_recipe_for(s1) + assert s1_recipe.args[0] == "Hello World" + assert s1_recipe.kwargs["val"] == 21 + + s2_recipe = test_registry.get_recipe_for(s2) + assert s2_recipe.args[0] == "Hello World" + assert s2_recipe.args[1] == 21 + + def test_default_impl(): s = TestLayer("Hello World", val=21) assert s.val == 21 assert s.class_name() == "TestLayerDefaultMode" + assert isinstance(s, TestLayerDefaultMode.class_) + assert isinstance(s, LayerContainer) def test_train_impl(): - bitorch.mode = RuntimeMode.INFERENCE_AUTO + bitorch.mode = TEST_MODE s = TestLayer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerOtherMode" + assert s.class_name() == "TestLayerCustomMode" + assert isinstance(s, TestLayerCustomMode.class_) + assert isinstance(s, LayerContainer) + +def test_raw_impl(): + bitorch.mode = RuntimeMode.RAW + s = TestLayer("Hello World", val=21) + assert s.val == 21 + assert s.class_name() == "TestLayerDefaultMode" + assert isinstance(s, TestLayer.class_) + assert not isinstance(s, LayerContainer) + + +@pytest.mark.parametrize("val, is_supported", [(150, False), (50, True)]) +def test_clone(val, is_supported): + s = TestLayer("Hello World", val=val) + s_recipe = test_registry.get_recipe_for(s) + if is_supported: + replacement = test_registry.get_replacement(TEST_MODE, s_recipe) + assert isinstance(replacement, TestLayerCustomMode.class_) # type: ignore + else: + with pytest.raises(RuntimeError) as e_info: + _ = test_registry.get_replacement(TEST_MODE, s_recipe) + error_message = str(e_info.value) + assert e_info.typename == "RuntimeError" + expected_key_strings = ["TestLayer", "layer implementation", str(TEST_MODE)] + assert all(key in error_message for key in expected_key_strings) diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py new file mode 100644 index 0000000..068c8ec --- /dev/null +++ b/tests/models/test_model_conversion.py @@ -0,0 +1,94 @@ +from typing import Any + +import pytest +from torch import nn +from torch.nn import functional as F + +import bitorch +import bitorch.runtime_mode +from bitorch import RuntimeMode +from bitorch.datasets import MNIST +from bitorch.layers import QConv2d, QLinear +from bitorch.layers.extensions.layer_implementation import LayerRecipe, CustomImplementation +from bitorch.layers.qlinear import QLinearImplementation, q_linear_registry, QLinearBase +from bitorch.models import Model + +TEST_MODE = RuntimeMode.INFERENCE_AUTO + + +class TestModel(Model): + def __init__(self): + super().__init__(dataset=MNIST) + self.q_conv2d = QConv2d(1, 32, 3, 1, 1) + self.q_linear = QLinear(784, 64) + self._model = nn.Sequential( + self.q_conv2d, + nn.Conv2d(1, 32, 3, 1, 1), + nn.Flatten(), + self.q_linear, + nn.Linear(64, 10), + ) + + def forward(self, x): + x = self._model(x) + output = F.log_softmax(x, dim=1) + return output + + +@pytest.fixture +def get_decorated_test_impl(): + @QLinearImplementation(TEST_MODE) + class QLinearTestImpl(CustomImplementation, nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + with bitorch.pause_wrapping(): + self._layer = QLinear(*args, **kwargs) + self.is_test_implementation = True + + def forward(self, x): + return self._layer(x) + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + return True + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + return cls(*recipe.args, **recipe.kwargs) + yield QLinearTestImpl + q_linear_registry.clear() + q_linear_registry.unregister_custom_implementations() + + +@pytest.fixture +def get_subclassed_test_impl(): + @QLinearImplementation(TEST_MODE) + class QLinearTestImpl(CustomImplementation, QLinearBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_test_implementation = True + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + return True + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + return cls(*recipe.args, **recipe.kwargs) + yield QLinearTestImpl + q_linear_registry.clear() + q_linear_registry.unregister_custom_implementations() + + +def test_convert_model_decorated(get_decorated_test_impl): + net = TestModel() + assert not hasattr(net.q_linear, "is_test_implementation") + net.convert(TEST_MODE) + assert hasattr(net.q_linear, "is_test_implementation") and net.q_linear.is_test_implementation + + +def test_convert_model_subclassed(get_subclassed_test_impl): + net = TestModel() + assert not hasattr(net.q_linear, "is_test_implementation") + net.convert(TEST_MODE) + assert hasattr(net.q_linear, "is_test_implementation") and net.q_linear.is_test_implementation diff --git a/tests/test_mode.py b/tests/test_runtime_mode.py similarity index 74% rename from tests/test_mode.py rename to tests/test_runtime_mode.py index ce036ef..02f763e 100644 --- a/tests/test_mode.py +++ b/tests/test_runtime_mode.py @@ -4,6 +4,9 @@ from bitorch import RuntimeMode +TEST_MODE = RuntimeMode.INFERENCE_AUTO + + def test_mode_creation_from_name(): for mode_str in RuntimeMode.__members__.keys(): assert isinstance(RuntimeMode.from_string(mode_str), RuntimeMode) @@ -17,7 +20,7 @@ def test_mode_supports_self(): def test_mode_does_not_support_other_mode(): for mode in RuntimeMode.__members__.values(): for other_mode in RuntimeMode.__members__.values(): - if mode == other_mode: + if mode == other_mode or mode == RuntimeMode.RAW or other_mode == RuntimeMode.RAW: continue assert not mode.is_supported_by(other_mode) @@ -51,14 +54,14 @@ def test_bitorch_default_mode(): @pytest.fixture() -def set_inference_mode(): - bitorch.mode = RuntimeMode.INFERENCE_AUTO +def set_test_mode(): + bitorch.mode = TEST_MODE yield None bitorch.mode = RuntimeMode.DEFAULT -def test_set_bitorch_mode(set_inference_mode): - assert bitorch.mode == RuntimeMode.INFERENCE_AUTO +def test_set_bitorch_mode(set_test_mode): + assert bitorch.mode == TEST_MODE @pytest.fixture() @@ -70,5 +73,15 @@ def reset_modes(): def test_setting_different_modes(reset_modes): assert bitorch.mode == RuntimeMode.DEFAULT - bitorch.mode = RuntimeMode.INFERENCE_AUTO - assert bitorch.mode == RuntimeMode.INFERENCE_AUTO + bitorch.mode = TEST_MODE + assert bitorch.mode == TEST_MODE + + +def test_with_statement(reset_modes): + with bitorch.change_mode(TEST_MODE): + assert bitorch.mode == TEST_MODE + + +def test_pause_wrap(reset_modes): + with bitorch.pause_wrapping(): + assert bitorch.mode == RuntimeMode.RAW From 0dcb11ed878aee5a5174a5f147171aa19d8f59eb Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 8 Jun 2022 17:23:02 +0200 Subject: [PATCH 022/208] update changelog --- CHANGELOG.md | 8 +++++++- version.txt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67084c6..cd0be92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.x.x] - Unreleased +## [0.3.x] - Unreleased + +### Added + +- Simple example script for MNIST +- Support for integration of bitorch's inference engine ### Changed - using PyTorch's implementation of RAdam +- renamed the `bitwidth` attribute of quantization functions to `bit_width` ### Fixed diff --git a/version.txt b/version.txt index 908d92b..3f0adea 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.0-dev.1 +0.2.0-dev.2 From 64439191653d2077afeece7ebd924789916b259a Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 10 Jun 2022 15:28:18 +0200 Subject: [PATCH 023/208] add decorators/registries for remaining layers, convert only layers belonging to the model --- .../layers/extensions/layer_implementation.py | 11 +- bitorch/layers/extensions/switchable_layer.py | 4 + bitorch/layers/qconv1d.py | 41 ++++++- bitorch/layers/qconv2d.py | 41 ++++++- bitorch/layers/qconv3d.py | 42 +++++++- bitorch/models/base.py | 9 +- examples/mnist/README.md | 2 +- tests/models/test_model_conversion.py | 102 +++++++++++++++--- 8 files changed, 216 insertions(+), 36 deletions(-) diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 3b96537..65238c2 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,6 +1,6 @@ from abc import ABC from dataclasses import dataclass -from typing import Any, Union, Set, Optional, Dict, Tuple, Type +from typing import Any, Union, Set, Optional, Dict, Tuple, Type, Iterable import torch from torch import nn @@ -233,9 +233,16 @@ def unregister_custom_implementations(self) -> None: for i in to_remove: self.layer_implementations.remove(i) - def convert_layers_to(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> None: + def convert_layers_to( + self, new_mode: RuntimeMode, + filter_: Optional[Iterable[Any]] = None, + device: torch.device = None, + verbose: bool = False + ) -> None: for recipe in self._instance_recipes: module = recipe.layer + if filter_ is not None and module.layer_implementation not in filter_: + continue assert isinstance(module, LayerContainer) if verbose: print("Converting", module) diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/switchable_layer.py index b672694..d45e06f 100644 --- a/bitorch/layers/extensions/switchable_layer.py +++ b/bitorch/layers/extensions/switchable_layer.py @@ -53,3 +53,7 @@ def __setattr__(self, key: Any, value: Any) -> None: @property # type: ignore[misc] def __class__(self) -> Any: return self._layer_implementation.__class__ + + @property + def layer_implementation(self) -> Any: + return self._layer_implementation diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 41a9c2b..7e92465 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -1,13 +1,16 @@ -"""Module containing the quantized convolution layer""" +"""Module containing the quantized 1d convolution layer""" from typing import Any, Union + from torch import Tensor from torch.nn import Conv1d, init from torch.nn.functional import pad, conv1d +from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.quantizations import Quantization +from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation +from bitorch.quantizations import Quantization class QConv1d_NoAct(Conv1d): # noqa: N801 @@ -67,7 +70,7 @@ def forward(self, input: Tensor) -> Tensor: groups=self.groups) -class QConv1d(QConv1d_NoAct): # type: ignore +class QConv1dBase(QConv1d_NoAct): # type: ignore def __init__(self, # type: ignore *args: Any, input_quantization: Union[str, Quantization] = None, @@ -84,7 +87,7 @@ def __init__(self, # type: ignore weight_quantization (Union[str, Quantization], optional): quantization module or name of quantization function for weights. Defaults to None. """ - super(QConv1d, self).__init__(*args, weight_quantization=weight_quantization, **kwargs) + super().__init__(*args, weight_quantization=weight_quantization, **kwargs) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) def forward(self, input_tensor: Tensor) -> Tensor: @@ -96,4 +99,32 @@ def forward(self, input_tensor: Tensor) -> Tensor: Returns: Tensor: the activated and convoluted output tensor. """ - return super(QConv1d, self).forward(self.activation(input_tensor)) + return super().forward(self.activation(input_tensor)) + + +q_conv1d_registry = LayerRegistry("QConv1d") + + +class QConv1dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv1d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv1d_registry, supports_modes) + + +@QConv1dImplementation(RuntimeMode.DEFAULT) +class QLinearDefaultImplementation(DefaultImplementation, QConv1dBase): + """ + This class defines the default implementation of a QConv1d layer (which is actually implemented by QConv1dBase). + + To implement a custom QConv1d implementation use QConv1dBase as a super class instead. + """ + pass + + +QConv1d = QLinearDefaultImplementation diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 6408880..767fe45 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -1,11 +1,16 @@ +"""Module containing the quantized 2d convolution layer""" + from typing import Union, Any + from torch import Tensor from torch.nn import Conv2d, init from torch.nn.functional import pad, conv2d +from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.quantizations import Quantization +from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation +from bitorch.quantizations import Quantization class QConv2d_NoAct(Conv2d): # type: ignore # noqa: N801 @@ -63,7 +68,7 @@ def forward(self, input: Tensor) -> Tensor: groups=self.groups) -class QConv2d(QConv2d_NoAct): # type: ignore +class QConv2dBase(QConv2d_NoAct): # type: ignore def __init__(self, # type: ignore *args: Any, input_quantization: Union[str, Quantization] = None, @@ -80,7 +85,7 @@ def __init__(self, # type: ignore weight_quantization (Union[str, Quantization], optional): quantization module or name of quantization function for weights. Defaults to None. """ - super(QConv2d, self).__init__(*args, weight_quantization=weight_quantization, **kwargs) + super().__init__(*args, weight_quantization=weight_quantization, **kwargs) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) def forward(self, input_tensor: Tensor) -> Tensor: @@ -92,4 +97,32 @@ def forward(self, input_tensor: Tensor) -> Tensor: Returns: Tensor: the activated and convoluted output tensor. """ - return super(QConv2d, self).forward(self.activation(input_tensor)) + return super().forward(self.activation(input_tensor)) + + +q_conv2d_registry = LayerRegistry("QConv2d") + + +class QConv2dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv2d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv2d_registry, supports_modes) + + +@QConv2dImplementation(RuntimeMode.DEFAULT) +class QLinearDefaultImplementation(DefaultImplementation, QConv2dBase): + """ + This class defines the default implementation of a QConv2d layer (which is actually implemented by QConv2dBase). + + To implement a custom QConv2d implementation use QConv2dBase as a super class instead. + """ + pass + + +QConv2d = QLinearDefaultImplementation diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 0db97b3..dc3bdd3 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -1,12 +1,16 @@ -"""Module containing the quantized convolution layer""" +"""Module containing the quantized 3d convolution layer""" + from typing import Union, Any + from torch import Tensor from torch.nn import Conv3d, init from torch.nn.functional import pad, conv3d +from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.quantizations import Quantization +from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation +from bitorch.quantizations import Quantization class QConv3d_NoAct(Conv3d): # type: ignore # noqa: N801 @@ -64,7 +68,7 @@ def forward(self, input: Tensor) -> Tensor: groups=self.groups) -class QConv3d(QConv3d_NoAct): # type: ignore +class QConv3dBase(QConv3d_NoAct): # type: ignore def __init__(self, # type: ignore *args: Any, input_quantization: Union[str, Quantization] = None, @@ -81,7 +85,7 @@ def __init__(self, # type: ignore weight_quantization (Union[str, Quantization], optional): quantization module or name of quantization function for weights. Defaults to None. """ - super(QConv3d, self).__init__(*args, weight_quantization=weight_quantization, **kwargs) + super().__init__(*args, weight_quantization=weight_quantization, **kwargs) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) def forward(self, input_tensor: Tensor) -> Tensor: @@ -93,4 +97,32 @@ def forward(self, input_tensor: Tensor) -> Tensor: Returns: Tensor: the activated and convoluted output tensor. """ - return super(QConv3d, self).forward(self.activation(input_tensor)) + return super().forward(self.activation(input_tensor)) + + +q_conv3d_registry = LayerRegistry("QConv3d") + + +class QConv3dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv3d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv3d_registry, supports_modes) + + +@QConv3dImplementation(RuntimeMode.DEFAULT) +class QLinearDefaultImplementation(DefaultImplementation, QConv3dBase): + """ + This class defines the default implementation of a QConv3d layer (which is actually implemented by QConv3dBase). + + To implement a custom QConv3d implementation use QConv3dBase as a super class instead. + """ + pass + + +QConv3d = QLinearDefaultImplementation diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 1fb6dbb..17f2a92 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -7,6 +7,10 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct +from bitorch.layers.extensions.switchable_layer import LayerContainer +from bitorch.layers.qconv1d import q_conv1d_registry +from bitorch.layers.qconv2d import q_conv2d_registry +from bitorch.layers.qconv3d import q_conv3d_registry from bitorch.layers.qlinear import q_linear_registry @@ -70,7 +74,8 @@ def initialize(self) -> None: nn.init.xavier_normal_(module.weight) def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": - for registry in (q_linear_registry, ): - registry.convert_layers_to(new_mode, device, verbose) + modules_to_replace = list(self.modules()) + for registry in (q_linear_registry, q_conv1d_registry, q_conv2d_registry, q_conv3d_registry): + registry.convert_layers_to(new_mode, filter_=modules_to_replace, device=device, verbose=verbose) self._model.to(device) return self diff --git a/examples/mnist/README.md b/examples/mnist/README.md index bf82356..cc566c9 100644 --- a/examples/mnist/README.md +++ b/examples/mnist/README.md @@ -1,6 +1,6 @@ # Example for MNIST -In this example script we train a simple model for the MNIST dataset and also use the inference engine for speed up. +In this example script we train a simple model for the MNIST dataset and also use the [bitorch inference engine](https://github.com/hpi-xnor/bitorch-inference-engine) for speed up. For example, run the following to train an MLP with 3 layers (one of which is a binary layer), or add `--help` for more arguments: diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 068c8ec..9d138a5 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -1,6 +1,7 @@ from typing import Any import pytest +import torch from torch import nn from torch.nn import functional as F @@ -10,6 +11,9 @@ from bitorch.datasets import MNIST from bitorch.layers import QConv2d, QLinear from bitorch.layers.extensions.layer_implementation import LayerRecipe, CustomImplementation +from bitorch.layers.qconv1d import q_conv1d_registry +from bitorch.layers.qconv2d import QConv2dBase, QConv2dImplementation, q_conv2d_registry +from bitorch.layers.qconv3d import q_conv3d_registry from bitorch.layers.qlinear import QLinearImplementation, q_linear_registry, QLinearBase from bitorch.models import Model @@ -23,7 +27,7 @@ def __init__(self): self.q_linear = QLinear(784, 64) self._model = nn.Sequential( self.q_conv2d, - nn.Conv2d(1, 32, 3, 1, 1), + nn.Conv2d(32, 1, 3, 1, 1), nn.Flatten(), self.q_linear, nn.Linear(64, 10), @@ -35,8 +39,16 @@ def forward(self, x): return output +def reset(): + bitorch.mode = RuntimeMode.DEFAULT + for registry in (q_linear_registry, q_conv2d_registry): + registry.unregister_custom_implementations() + + @pytest.fixture -def get_decorated_test_impl(): +def get_decorated_impls(): + reset() + @QLinearImplementation(TEST_MODE) class QLinearTestImpl(CustomImplementation, nn.Module): def __init__(self, *args, **kwargs): @@ -54,14 +66,40 @@ def can_clone(cls, recipe: LayerRecipe) -> bool: @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: - return cls(*recipe.args, **recipe.kwargs) - yield QLinearTestImpl - q_linear_registry.clear() - q_linear_registry.unregister_custom_implementations() + new_layer = cls(*recipe.args, **recipe.kwargs) + new_layer._layer.weight = recipe.layer.weight + new_layer._layer.bias = recipe.layer.bias + return new_layer + + @QConv2dImplementation(TEST_MODE) + class QConv2dTestImpl(CustomImplementation, nn.Module): + def __init__(self, *args, **kwargs): + super().__init__() + with bitorch.pause_wrapping(): + self._layer = QConv2d(*args, **kwargs) + self.is_test_implementation = True + + def forward(self, x): + return self._layer(x) + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + return True + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + new_layer = cls(*recipe.args, **recipe.kwargs) + new_layer._layer.weight = recipe.layer.weight + new_layer._layer.bias = recipe.layer.bias + return new_layer + yield QLinearTestImpl, QConv2dTestImpl + reset() @pytest.fixture -def get_subclassed_test_impl(): +def get_subclassed_impls(): + reset() + @QLinearImplementation(TEST_MODE) class QLinearTestImpl(CustomImplementation, QLinearBase): def __init__(self, *args, **kwargs): @@ -74,21 +112,51 @@ def can_clone(cls, recipe: LayerRecipe) -> bool: @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: - return cls(*recipe.args, **recipe.kwargs) - yield QLinearTestImpl - q_linear_registry.clear() - q_linear_registry.unregister_custom_implementations() + new_layer = cls(*recipe.args, **recipe.kwargs) + new_layer.weight = recipe.layer.weight + new_layer.bias = recipe.layer.bias + return new_layer + + @QConv2dImplementation(TEST_MODE) + class QConv2dTestImpl(CustomImplementation, QConv2dBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_test_implementation = True + + @classmethod + def can_clone(cls, recipe: LayerRecipe) -> bool: + return True + + @classmethod + def create_clone_from(cls, recipe: LayerRecipe) -> Any: + new_layer = cls(*recipe.args, **recipe.kwargs) + new_layer.weight = recipe.layer.weight + new_layer.bias = recipe.layer.bias + return new_layer + yield QLinearTestImpl, QConv2dTestImpl + reset() -def test_convert_model_decorated(get_decorated_test_impl): +def _test(): + x = torch.rand(1, 1, 28, 28) net = TestModel() + assert not hasattr(net.q_linear, "is_test_implementation") + assert not hasattr(net.q_conv2d, "is_test_implementation") + y1 = net(x) + net.convert(TEST_MODE) + assert hasattr(net.q_linear, "is_test_implementation") and net.q_linear.is_test_implementation + assert hasattr(net.q_conv2d, "is_test_implementation") and net.q_conv2d.is_test_implementation + y2 = net(x) + assert torch.equal(y1, y2) -def test_convert_model_subclassed(get_subclassed_test_impl): - net = TestModel() - assert not hasattr(net.q_linear, "is_test_implementation") - net.convert(TEST_MODE) - assert hasattr(net.q_linear, "is_test_implementation") and net.q_linear.is_test_implementation + +def test_convert_model_decorated(get_decorated_impls): + _test() + + +def test_convert_model_subclassed(get_subclassed_impls): + _test() From b409f6896b81a955efb8bebec5b41dd8dbd860ff Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 13 Jun 2022 15:10:18 +0200 Subject: [PATCH 024/208] add more docs --- bitorch/layers/extensions/__init__.py | 12 +++++++++++ ...switchable_layer.py => layer_container.py} | 20 +++++++++++++++++++ .../layers/extensions/layer_implementation.py | 12 +++++++++-- bitorch/models/base.py | 2 +- docs/source/conf.py | 2 +- docs/source/index.rst | 4 ++-- tests/layers/test_layer_impl.py | 2 +- tests/layers/test_switchable_layer.py | 2 +- 8 files changed, 48 insertions(+), 8 deletions(-) rename bitorch/layers/extensions/{switchable_layer.py => layer_container.py} (73%) diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py index e69de29..b3e1305 100644 --- a/bitorch/layers/extensions/__init__.py +++ b/bitorch/layers/extensions/__init__.py @@ -0,0 +1,12 @@ +""" +This submodule contains objects needed to provide and manage custom layer implementations. +""" + +from .layer_container import LayerContainer +from .layer_implementation import ( + LayerImplementation, + LayerRegistry, + LayerRecipe, + DefaultImplementation, + CustomImplementation, +) diff --git a/bitorch/layers/extensions/switchable_layer.py b/bitorch/layers/extensions/layer_container.py similarity index 73% rename from bitorch/layers/extensions/switchable_layer.py rename to bitorch/layers/extensions/layer_container.py index d45e06f..6141906 100644 --- a/bitorch/layers/extensions/switchable_layer.py +++ b/bitorch/layers/extensions/layer_container.py @@ -2,10 +2,25 @@ class LayerContainer: + """ + This class wraps another layer - but the internally contained class can be swapped out during runtime. + """ def __init__(self, impl_class: Any, *args: Any, **kwargs: Any) -> None: + """ + Wrap a new object based on the given class, positional arguments, and keyword arguments. + Args: + impl_class: class of the new object + *args: positional arguments of the new object + **kwargs: keyword arguments of the new object + """ self._layer_implementation = impl_class(*args, **kwargs) def replace_layer_implementation(self, new_implementation: Any) -> None: + """ + Replace the internally stored layer object with the given one. + Args: + new_implementation: new class which should replace the previous implementation. + """ self._layer_implementation = new_implementation def __getattr__(self, item: Any) -> Any: @@ -56,4 +71,9 @@ def __class__(self) -> Any: @property def layer_implementation(self) -> Any: + """ + Access the internally wrapped layer object directly. + Returns: + the internal layer object + """ return self._layer_implementation diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 65238c2..553f9c7 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -7,7 +7,7 @@ import bitorch from bitorch import runtime_mode_type, RuntimeMode -from .switchable_layer import LayerContainer +from .layer_container import LayerContainer @dataclass(eq=False, frozen=True) @@ -22,10 +22,10 @@ class LayerRecipe: class BaseImplementation: + """Defines the class interface of a custom layer implementation of a certain layer type.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - """Defines the class interface of a custom layer implementation of a certain layer type.""" @classmethod def is_default_implementation(cls) -> bool: """ @@ -157,6 +157,14 @@ def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerConta return correct_layer_implementation._provide_layer_implementation(*args, **kwargs) def supports_mode(self, mode: RuntimeMode) -> bool: + """ + Check whether the stored layer implementation supports a given RuntimeMode. + Args: + mode: + + Returns: + + """ return mode.is_supported_by(self._supported_modes) def can_create_clone_from(self, recipe: LayerRecipe) -> bool: diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 17f2a92..8634e98 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -7,7 +7,7 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct -from bitorch.layers.extensions.switchable_layer import LayerContainer +from bitorch.layers.extensions.layer_container import LayerContainer from bitorch.layers.qconv1d import q_conv1d_registry from bitorch.layers.qconv2d import q_conv2d_registry from bitorch.layers.qconv3d import q_conv3d_registry diff --git a/docs/source/conf.py b/docs/source/conf.py index 75d96d5..2e7c355 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,7 +68,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # # This is the expected signature of the handler for this event, cf doc diff --git a/docs/source/index.rst b/docs/source/index.rst index b5d725d..86dfab1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ Welcome to bitorch's documentation! BITorch is a library currently under development to simplify building quantized and binary neural networks -with [PyTorch](https://pytorch.org/). +with `PyTorch `_. This is an early preview version of the library. If you wish to use it and encounter any problems, please create an issue. Our current roadmap contains: @@ -16,7 +16,7 @@ Our current roadmap contains: - Extending the model zoo with pre-trained models of state-of-the-art approaches - Adding examples for advanced training methods with multiple stages, knowledge distillation, etc. -All changes are tracked in the [changelog](CHANGELOG.md). +All changes are tracked in the `changelog `_. diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index bc00025..9aeae7c 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -6,7 +6,7 @@ from bitorch import RuntimeMode from bitorch.layers.extensions.layer_implementation import LayerImplementation, LayerRegistry, \ CustomImplementation, DefaultImplementation, LayerRecipe -from bitorch.layers.extensions.switchable_layer import LayerContainer +from bitorch.layers.extensions.layer_container import LayerContainer TEST_MODE = RuntimeMode.INFERENCE_AUTO diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index 5e9c4f9..ec5c72f 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -2,7 +2,7 @@ import torch from torch import nn -from bitorch.layers.extensions.switchable_layer import LayerContainer +from bitorch.layers.extensions.layer_container import LayerContainer class Foo: From f76d08e97626fab766ff5e58802f1c05ff0559f4 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 13 Jun 2022 15:18:27 +0200 Subject: [PATCH 025/208] use import from layer extensions --- bitorch/layers/qconv1d.py | 2 +- bitorch/layers/qconv2d.py | 2 +- bitorch/layers/qconv3d.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 7e92465..220bd73 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 767fe45..fc95441 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index dc3bdd3..7a8c979 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions.layer_implementation import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization From 0b57781cbd23500208b4d2d88949ef1a3e9c361c Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 14 Jun 2022 14:31:38 +0200 Subject: [PATCH 026/208] update readme --- AUTHORS | 10 ++++++++++ README.md | 27 +++++++++++++++++---------- examples/mnist/train_mnist.py | 5 ++++- setup.py | 5 +++-- 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..53f2c1d --- /dev/null +++ b/AUTHORS @@ -0,0 +1,10 @@ +The development of BITorch was started by Joseph Bethge and PD Dr. Haojin Yang. + +The current maintainers can be contacted at: fb10-xnor@hpi.de + +The following people have contributed code to BITorch (in alphabetical order): + + Christopher Aust + Joseph Bethge + Paul Mattes + Haojing Yang diff --git a/README.md b/README.md index e34dd04..6d65ad2 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,8 @@ The package can also be installed locally for editing and development. First, clone the [repository](https://github.com/hpi-xnor/bitorch), then run: ```bash -pip install -e . -``` - -To activate advanced logging with Tensorboard and model summary, install the optional dependencies as well: - -```bash -pip install -e ".[opt]" +pip install -e . # without optional dependencies +pip install -e ".[opt]" # with optional dependencies ``` ### Dali Preprocessing @@ -60,17 +55,19 @@ e.g. with CUDA 11.x, (currently only supported for imagenet) you need to install the `nvidia-dali-cuda110` package by running the following command: ``` - pip install --extra-index-url https://developer.download.nvidia.com/compute/redist --upgrade nvidia-dali-cuda110 +pip install --extra-index-url https://developer.download.nvidia.com/compute/redist --upgrade nvidia-dali-cuda110 ``` -### Code formatting and typing +## Development -Install the _dev_ requirements for (local) development: +Install the package and _dev_ requirements locally for development: ```bash pip install -e ".[dev]" ``` +### Code formatting and typing + New code should be compatible with Python 3.X versions and be compliant with PEP8. To check the codebase, please run ```bash @@ -88,3 +85,13 @@ Finally, the tests can be run with: ```bash pytest ``` + +### Documentation + +We use [Google's Python Docstring Format](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) +to document our code. + +Documentation can be generated with +```bash +sphinx-build -b html docs/source/ docs/build/ -a +``` diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 31bae82..3dbd424 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -1,5 +1,8 @@ """ -Adapted from official PyTorch example: https://github.com/pytorch/examples/blob/main/mnist/main.py +An example script for training a model for the MNIST dataset with BITorch. + +Modified from the `PyTorch MNIST Example `_, +which was published under the `BSD 3-Clause License `_. """ import argparse diff --git a/setup.py b/setup.py index ef1f906..dda74ec 100644 --- a/setup.py +++ b/setup.py @@ -27,8 +27,8 @@ def get_requirements(file_path: Union[Path, str]): name="bitorch", url="https://github.com/hpi-xnor/bitorch", version=version, - author="Joseph Bethge", - author_email="joseph.bethge@hpi.de", + author="Hasso Plattner Institute", + author_email="fb10-xnor@hpi.de", description="A package for building and training quantized and binary neural networks with Pytorch", long_description=readme_content, long_description_content_type="text/markdown", @@ -45,6 +45,7 @@ def get_requirements(file_path: Union[Path, str]): "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], python_requires='>=3.7', data_files=[ From 6a8ab542e3daef34efbf04d223c27b11ab8aa96e Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 15 Jun 2022 15:31:14 +0200 Subject: [PATCH 027/208] split up classes and add better typing --- bitorch/__init__.py | 1 + bitorch/layers/__init__.py | 52 ++++- bitorch/layers/config.py | 9 +- bitorch/layers/extensions/__init__.py | 11 +- bitorch/layers/extensions/layer_container.py | 16 +- .../layers/extensions/layer_implementation.py | 202 +----------------- bitorch/layers/extensions/layer_recipe.py | 22 ++ .../layers/extensions/layer_registration.py | 100 +++++++++ bitorch/layers/extensions/layer_registry.py | 91 ++++++++ bitorch/layers/qactivation.py | 10 +- bitorch/layers/qconv1d.py | 2 +- bitorch/layers/qconv2d.py | 2 +- bitorch/layers/qconv3d.py | 2 +- bitorch/layers/qlinear.py | 37 +++- bitorch/models/base.py | 8 +- bitorch/runtime_mode.py | 6 +- examples/mnist/train_mnist.py | 3 + tests/layers/test_layer_impl.py | 44 ++-- tests/layers/test_qactivation.py | 12 +- tests/layers/test_qlinear.py | 3 +- tests/models/test_model_conversion.py | 3 +- 21 files changed, 369 insertions(+), 267 deletions(-) create mode 100644 bitorch/layers/extensions/layer_recipe.py create mode 100644 bitorch/layers/extensions/layer_registration.py create mode 100644 bitorch/layers/extensions/layer_registry.py diff --git a/bitorch/__init__.py b/bitorch/__init__.py index 05e1c50..fdb2a26 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -6,6 +6,7 @@ from .config import Config from .runtime_mode import RuntimeMode, runtime_mode_type, change_mode, pause_wrapping +from .layers import convert mode: RuntimeMode = RuntimeMode.DEFAULT diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index dd7e809..9043932 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -3,6 +3,10 @@ and activations before forwarding them. These layers use the quantization functions specified in the quantization submodule. """ +from typing import List, TypeVar + +import torch +from torch import nn from .debug_layers import ( InputGraphicalDebug, @@ -11,19 +15,51 @@ WeightPrintDebug, ShapePrintDebug ) -from .qactivation import QActivation -from .qconv1d import QConv1d, QConv1d_NoAct -from .qconv2d import QConv2d, QConv2d_NoAct -from .qconv3d import QConv3d, QConv3d_NoAct -from .qlinear import QLinear, QLinearBase +from .extensions import CustomImplementation, LayerRegistry from .pact import Pact +from .qactivation import QActivation +from .qconv1d import QConv1d, QConv1d_NoAct, q_conv1d_registry +from .qconv2d import QConv2d, QConv2d_NoAct, q_conv2d_registry +from .qconv3d import QConv3d, QConv3d_NoAct, q_conv3d_registry from .qembedding import QEmbedding, QEmbeddingBag - -from .extensions.layer_implementation import CustomImplementation +from .qlinear import QLinear, QLinearBase, q_linear_registry __all__ = [ "InputGraphicalDebug", "InputPrintDebug", "WeightGraphicalDebug", "WeightPrintDebug", "ShapePrintDebug", "QActivation", "QConv1d", "QConv2d", "QConv3d", "QConv1d_NoAct", "QConv2d_NoAct", "QConv3d_NoAct", "QLinear", "QLinearBase", "QEmbedding", "QEmbeddingBag", "Pact", - "CustomImplementation" + "CustomImplementation", "convert" ] + +from .. import RuntimeMode + + +def _get_layer_registries() -> List[LayerRegistry]: + return [ + q_conv1d_registry, + q_conv2d_registry, + q_conv3d_registry, + q_linear_registry, + ] + + +T = TypeVar("T", bound=nn.Module) + + +def convert(module: T, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> T: + """ + Convert the given module to a new bitorch RuntimeMode. Needs to have custom implementations installed. + Args: + module: the module to be converted + new_mode: the new mode for the module + device: an optional device + verbose: whether to print which layers are converted + + Returns: + the converted module + """ + submodules = list(module.modules()) + for registry in _get_layer_registries(): + registry.convert_layers_to(new_mode, only=submodules, device=device, verbose=verbose) + module.to(device) + return module diff --git a/bitorch/layers/config.py b/bitorch/layers/config.py index 9fb5f84..05b34c9 100644 --- a/bitorch/layers/config.py +++ b/bitorch/layers/config.py @@ -1,7 +1,6 @@ """Config class for quantization layers. This file should be imported before the other layers.""" from typing import Union -import torch from bitorch.config import Config from bitorch.quantizations import quantization_from_name, Quantization @@ -12,14 +11,14 @@ class LayerConfig(Config): name = "layer_config" - def get_quantization_function(self, quantization: Union[str, Quantization]) -> torch.nn.Module: - """Returns the quanitization module specified in quantization_name. + def get_quantization_function(self, quantization: Union[str, Quantization]) -> Quantization: + """Returns the quantization module specified in quantization_name. Args: - quantization (Union[str, Quantization]): quantization module or name of quantization function. + quantization: quantization module or name of quantization function. Returns: - torch.nn.Module: Quantization module + the quantization module """ if isinstance(quantization, Quantization): return quantization diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py index b3e1305..9f82267 100644 --- a/bitorch/layers/extensions/__init__.py +++ b/bitorch/layers/extensions/__init__.py @@ -3,10 +3,7 @@ """ from .layer_container import LayerContainer -from .layer_implementation import ( - LayerImplementation, - LayerRegistry, - LayerRecipe, - DefaultImplementation, - CustomImplementation, -) +from .layer_implementation import DefaultImplementation, CustomImplementation +from .layer_registration import LayerImplementation +from .layer_registry import LayerRegistry +from .layer_recipe import LayerRecipe diff --git a/bitorch/layers/extensions/layer_container.py b/bitorch/layers/extensions/layer_container.py index 6141906..895825e 100644 --- a/bitorch/layers/extensions/layer_container.py +++ b/bitorch/layers/extensions/layer_container.py @@ -1,11 +1,13 @@ -from typing import Any +from typing import Any, TypeVar, Type, Generic +T = TypeVar("T") -class LayerContainer: + +class LayerContainer(Generic[T]): """ This class wraps another layer - but the internally contained class can be swapped out during runtime. """ - def __init__(self, impl_class: Any, *args: Any, **kwargs: Any) -> None: + def __init__(self, impl_class: Type[T], *args: Any, **kwargs: Any) -> None: """ Wrap a new object based on the given class, positional arguments, and keyword arguments. Args: @@ -15,7 +17,7 @@ def __init__(self, impl_class: Any, *args: Any, **kwargs: Any) -> None: """ self._layer_implementation = impl_class(*args, **kwargs) - def replace_layer_implementation(self, new_implementation: Any) -> None: + def replace_layer_implementation(self, new_implementation: T) -> None: """ Replace the internally stored layer object with the given one. Args: @@ -57,7 +59,7 @@ def __repr__(self) -> "str": return f"LayerContainer (at {hex(id(self))}), contains: {self._layer_implementation}" def __call__(self, *args: Any, **kwargs: Any) -> Any: - return self._layer_implementation(*args, **kwargs) + return self._layer_implementation(*args, **kwargs) # type:ignore[operator] def __setattr__(self, key: Any, value: Any) -> None: if key == "_layer_implementation": @@ -66,11 +68,11 @@ def __setattr__(self, key: Any, value: Any) -> None: setattr(self._layer_implementation, key, value) @property # type: ignore[misc] - def __class__(self) -> Any: + def __class__(self) -> Type[T]: # type: ignore return self._layer_implementation.__class__ @property - def layer_implementation(self) -> Any: + def layer_implementation(self) -> T: """ Access the internally wrapped layer object directly. Returns: diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 553f9c7..2643cdc 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,24 +1,8 @@ from abc import ABC -from dataclasses import dataclass -from typing import Any, Union, Set, Optional, Dict, Tuple, Type, Iterable +from typing import Any, TYPE_CHECKING -import torch -from torch import nn - -import bitorch -from bitorch import runtime_mode_type, RuntimeMode -from .layer_container import LayerContainer - - -@dataclass(eq=False, frozen=True) -class LayerRecipe: - """ - Data class to store a layer object and the arguments used to create it. - It allows to create other implementations of the same layer later on. - """ - layer: "LayerContainer" - args: Tuple[Any, ...] - kwargs: Dict[str, Any] +if TYPE_CHECKING: + from . import LayerRecipe class BaseImplementation: @@ -35,7 +19,7 @@ def is_default_implementation(cls) -> bool: raise NotImplementedError("Should be implemented by subclass.") @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: + def can_clone(cls, recipe: "LayerRecipe") -> bool: """ Returns whether this layer class supports the implementation of a given layer recipe. @@ -48,7 +32,7 @@ def can_clone(cls, recipe: LayerRecipe) -> bool: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe") -> Any: """ Create a new layer based on a given layer recipe (can be expected to be from the default category). @@ -68,11 +52,11 @@ def is_default_implementation(cls) -> bool: return True @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: + def can_clone(cls, recipe: "LayerRecipe") -> bool: return True @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe") -> Any: return cls(*recipe.args, **recipe.kwargs) @@ -83,177 +67,9 @@ def is_default_implementation(cls) -> bool: return False @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: + def can_clone(cls, recipe: "LayerRecipe") -> bool: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe") -> Any: raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") - - -class LayerImplementation(ABC): - """ - Superclass for storing different implementations of a common layer type. - - It registers all decorated classes in the given registry. On creation of a decorated class, it - wraps the created class object in a layer container and stores the arguments used to create the layer. - """ - - registry: "LayerRegistry" - class_: Type[BaseImplementation] - class_name: str - _supported_modes: runtime_mode_type - __initialized: bool - - def __init__(self, registry: "LayerRegistry", supported_modes: runtime_mode_type) -> None: - """ - Define an implementation decorator for a certain type of layer. All implementations and objects of this type of - layer are stored in the given registry. - - Args: - registry: the registry which should store the implementation and objects of this layer type - supported_modes: the mode supported by the registering implementation - """ - self.registry = registry - assert RuntimeMode.is_combined_mode(supported_modes), f"invalid mode {supported_modes} given" - self._supported_modes = supported_modes - self.__initialized = False - self.class_ = None # type: ignore - self.class_name = "" - - def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", LayerContainer, nn.Module]: - if not self.__initialized: - # this object is called once when @Decorator is used, we need to initialize - return self._initialize(*args, **kwargs) - - if bitorch.mode == RuntimeMode.RAW: - return self.class_(*args, **kwargs) # type: ignore - - # on later calls we need to provide the correct layer implementation - return self._provide_layer_implementation(*args, **kwargs) - - def _initialize(self, class_: Type[BaseImplementation]) -> "LayerImplementation": - self.__initialized = True - self.class_ = class_ - self.class_name = self.class_.__name__ - if self._supported_modes == RuntimeMode.DEFAULT: - assert issubclass(self.class_, DefaultImplementation), \ - f"{self.class_name} should be a subclass of DefaultLayerImplementation." - else: - assert issubclass(self.class_, CustomImplementation), \ - f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " \ - f"implement the corresponding class methods)." - self.registry.register(self) - return self - - def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerContainer: - correct_layer_implementation = self.registry.get_layer() - if self == correct_layer_implementation: - # this class provides the correct implementation for the current mode (recursion stop) - layer_container = LayerContainer(self.class_, *args, **kwargs) - self.registry.add_recipe(LayerRecipe(layer=layer_container, args=args, kwargs=kwargs)) - return layer_container - # call this method again but on the correct base class - return correct_layer_implementation._provide_layer_implementation(*args, **kwargs) - - def supports_mode(self, mode: RuntimeMode) -> bool: - """ - Check whether the stored layer implementation supports a given RuntimeMode. - Args: - mode: - - Returns: - - """ - return mode.is_supported_by(self._supported_modes) - - def can_create_clone_from(self, recipe: LayerRecipe) -> bool: - return self.class_.can_clone(recipe) - - def get_replacement(self, recipe: LayerRecipe) -> Any: - return self.class_.create_clone_from(recipe) - - def is_default(self) -> bool: - return self.class_.is_default_implementation() - - -class LayerRegistry: - """ - Stores all available implementations (and their supported modes) for a certain type of layer. - It also wraps these implementations and stores references to them, so they can be replaced easily. - Needs to be subclassed for each type of layer. - """ - def __init__(self, name: str) -> None: - self.name = name - self._class = None - self.layer_implementations: Set[LayerImplementation] = set() - self._instance_recipes: Set[LayerRecipe] = set() - self.is_replacing = False - - @property - def layer_instances(self) -> Set["LayerContainer"]: - return set(x.layer for x in self._instance_recipes) - - def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: - if layer not in map(lambda x: x.layer, self._instance_recipes): - return None - return next(filter(lambda x: x.layer == layer, self._instance_recipes)) - - def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe) -> Any: - layer = self.get_layer(mode, recipe) - return layer.get_replacement(recipe) - - def add_recipe(self, new_recipe: LayerRecipe) -> None: - if self.is_replacing: - return - self._instance_recipes.add(new_recipe) - - def __contains__(self, item: Any) -> bool: - return item.__class__ in map(lambda x: x.class_, self.layer_implementations) - - def register(self, layer: LayerImplementation) -> None: - self.layer_implementations.add(layer) - - def get_layer( - self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None - ) -> LayerImplementation: - if mode is None: - mode = bitorch.mode - available_layers = [] - for implementation in self.layer_implementations: - if not implementation.supports_mode(mode): - continue - if recipe and not implementation.can_create_clone_from(recipe): - continue - available_layers.append(implementation) - if len(available_layers) > 1: - RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") - if len(available_layers) == 0: - raise RuntimeError(f"No layer implementation for '{self.name}' available (mode='{mode}').") - return available_layers[0] - - def clear(self) -> None: - while len(self._instance_recipes) > 0: - self._instance_recipes.pop() - - def unregister_custom_implementations(self) -> None: - to_remove = list(filter(lambda x: not x.is_default(), self.layer_implementations)) - for i in to_remove: - self.layer_implementations.remove(i) - - def convert_layers_to( - self, new_mode: RuntimeMode, - filter_: Optional[Iterable[Any]] = None, - device: torch.device = None, - verbose: bool = False - ) -> None: - for recipe in self._instance_recipes: - module = recipe.layer - if filter_ is not None and module.layer_implementation not in filter_: - continue - assert isinstance(module, LayerContainer) - if verbose: - print("Converting", module) - replacement_module = self.get_replacement(new_mode, recipe) - replacement_module.to(device) - module.replace_layer_implementation(replacement_module) diff --git a/bitorch/layers/extensions/layer_recipe.py b/bitorch/layers/extensions/layer_recipe.py new file mode 100644 index 0000000..c5da665 --- /dev/null +++ b/bitorch/layers/extensions/layer_recipe.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from typing import TypeVar, Tuple, Any, Dict + +from .layer_container import LayerContainer + +T = TypeVar("T") + + +@dataclass(eq=False, frozen=True) +class LayerRecipe: + """ + Data class to store a layer object and the arguments used to create it. + It allows to create other implementations of the same layer later on. + """ + layer: "LayerContainer" + args: Tuple[Any, ...] + kwargs: Dict[str, Any] + + def get_by_position_or_key(self, pos: int, key: str, default: T) -> T: + if len(self.args) > pos: + return self.args[pos] + return self.kwargs.get(key, default) diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py new file mode 100644 index 0000000..5c0416e --- /dev/null +++ b/bitorch/layers/extensions/layer_registration.py @@ -0,0 +1,100 @@ +from abc import ABC +from typing import Any, Type, Union, TYPE_CHECKING + +import bitorch +from bitorch import runtime_mode_type, RuntimeMode +from .layer_container import LayerContainer +from .layer_implementation import DefaultImplementation, BaseImplementation, CustomImplementation +from .layer_recipe import LayerRecipe + +if TYPE_CHECKING: + from .layer_registry import LayerRegistry + + +class LayerImplementation(ABC): + """ + Superclass for storing different implementations of a common layer type. + + It registers all decorated classes in the given registry. On creation of a decorated class, it + wraps the created class object in a layer container and stores the arguments used to create the layer. + """ + + registry: "LayerRegistry" + class_: Type[BaseImplementation] + class_name: str + _supported_modes: runtime_mode_type + __initialized: bool + + def __init__(self, registry: "LayerRegistry", supported_modes: runtime_mode_type) -> None: + """ + Define an implementation decorator for a certain type of layer. All implementations and objects of this type of + layer are stored in the given registry. + + Args: + registry: the registry which should store the implementation and objects of this layer type + supported_modes: the mode supported by the registering implementation + """ + self.registry = registry + assert RuntimeMode.is_combined_mode(supported_modes), f"invalid mode {supported_modes} given" + self._supported_modes = supported_modes + self.__initialized = False + self.class_ = None # type: ignore + self.class_name = "" + + def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", Type[BaseImplementation], LayerContainer]: + if not self.__initialized: + # this object is called once when @Decorator is used, we need to initialize + return self._initialize(*args, **kwargs) + + if bitorch.mode == RuntimeMode.RAW: + return self.class_(*args, **kwargs) # type: ignore + + # on later calls we need to provide the correct layer implementation + return self._provide_layer_implementation(*args, **kwargs) + + def _initialize(self, class_: Type[BaseImplementation]) -> Union["LayerImplementation", Type[BaseImplementation]]: + self.__initialized = True + self.class_ = class_ + self.class_name = self.class_.__name__ + self.registry.register(self) + if self._supported_modes == RuntimeMode.DEFAULT: + assert issubclass(self.class_, DefaultImplementation), \ + f"{self.class_name} should be a subclass of DefaultLayerImplementation." + # provide this wrapper + return self + else: + assert issubclass(self.class_, CustomImplementation), \ + f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " \ + f"implement the corresponding class methods)." + # after we have registered custom implementations, we do not interfere anymore + return self.class_ + + def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerContainer: + correct_layer_implementation = self.registry.get_layer() + if self == correct_layer_implementation: + # this class provides the correct implementation for the current mode (recursion stop) + layer_container = LayerContainer(self.class_, *args, **kwargs) + self.registry.add_recipe(LayerRecipe(layer=layer_container, args=args, kwargs=kwargs)) + return layer_container + # call this method again but on the correct base class + return correct_layer_implementation._provide_layer_implementation(*args, **kwargs) + + def supports_mode(self, mode: RuntimeMode) -> bool: + """ + Check whether the stored layer implementation supports a given RuntimeMode. + Args: + mode: + + Returns: + + """ + return mode.is_supported_by(self._supported_modes) + + def can_create_clone_from(self, recipe: LayerRecipe) -> bool: + return self.class_.can_clone(recipe) + + def get_replacement(self, recipe: LayerRecipe) -> Any: + return self.class_.create_clone_from(recipe) + + def is_default(self) -> bool: + return self.class_.is_default_implementation() diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py new file mode 100644 index 0000000..7c571e3 --- /dev/null +++ b/bitorch/layers/extensions/layer_registry.py @@ -0,0 +1,91 @@ +from typing import Set, Any, Optional, Iterable + +import torch + +import bitorch +from bitorch import RuntimeMode +from .layer_container import LayerContainer +from .layer_recipe import LayerRecipe +from .layer_registration import LayerImplementation + + +class LayerRegistry: + """ + Stores all available implementations (and their supported modes) for a certain type of layer. + It also wraps these implementations and stores references to them, so they can be replaced easily. + Needs to be subclassed for each type of layer. + """ + def __init__(self, name: str) -> None: + self.name = name + self._class = None + self.layer_implementations: Set[LayerImplementation] = set() + self._instance_recipes: Set[LayerRecipe] = set() + self.is_replacing = False + + @property + def layer_instances(self) -> Set["LayerContainer"]: + return set(x.layer for x in self._instance_recipes) + + def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: + if layer not in map(lambda x: x.layer, self._instance_recipes): + return None + return next(filter(lambda x: x.layer == layer, self._instance_recipes)) + + def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe) -> Any: + layer = self.get_layer(mode, recipe) + return layer.get_replacement(recipe) + + def add_recipe(self, new_recipe: LayerRecipe) -> None: + if self.is_replacing: + return + self._instance_recipes.add(new_recipe) + + def __contains__(self, item: Any) -> bool: + return item.__class__ in map(lambda x: x.class_, self.layer_implementations) + + def register(self, layer: LayerImplementation) -> None: + self.layer_implementations.add(layer) + + def get_layer( + self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None + ) -> LayerImplementation: + if mode is None: + mode = bitorch.mode + available_layers = [] + for implementation in self.layer_implementations: + if not implementation.supports_mode(mode): + continue + if recipe and not implementation.can_create_clone_from(recipe): + continue + available_layers.append(implementation) + if len(available_layers) > 1: + RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") + if len(available_layers) == 0: + raise RuntimeError(f"No implementation for '{self.name}' available (mode='{mode}').") + return available_layers[0] + + def clear(self) -> None: + while len(self._instance_recipes) > 0: + self._instance_recipes.pop() + + def unregister_custom_implementations(self) -> None: + to_remove = list(filter(lambda x: not x.is_default(), self.layer_implementations)) + for i in to_remove: + self.layer_implementations.remove(i) + + def convert_layers_to( + self, new_mode: RuntimeMode, + only: Optional[Iterable[Any]] = None, + device: torch.device = None, + verbose: bool = False + ) -> None: + for recipe in list(self._instance_recipes): + module = recipe.layer + if only is not None and module.layer_implementation not in only: + continue + assert isinstance(module, LayerContainer) + if verbose: + print("Converting", module) + replacement_module = self.get_replacement(new_mode, recipe) + replacement_module.to(device) + module.replace_layer_implementation(replacement_module) diff --git a/bitorch/layers/qactivation.py b/bitorch/layers/qactivation.py index 0e80e2d..4833c1b 100644 --- a/bitorch/layers/qactivation.py +++ b/bitorch/layers/qactivation.py @@ -67,8 +67,8 @@ def __init__( cancellation. Disabled if threshold is 0. """ super(QActivation, self).__init__() - self._activation = config.get_quantization_function(activation or config.input_quantization) - self._gradient_cancellation_threshold = ( + self.activation_function = config.get_quantization_function(activation or config.input_quantization) + self.gradient_cancellation_threshold = ( gradient_cancellation_threshold or config.gradient_cancellation_threshold ) @@ -81,6 +81,6 @@ def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: quantized input tensor. """ - if self._gradient_cancellation_threshold > 0: - input_tensor = GradientCancellation.apply(input_tensor, self._gradient_cancellation_threshold) - return self._activation(input_tensor) + if self.gradient_cancellation_threshold > 0: + input_tensor = GradientCancellation.apply(input_tensor, self.gradient_cancellation_threshold) + return self.activation_function(input_tensor) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 220bd73..4d112dd 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index fc95441..c70f500 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 7a8c979..b81ed7c 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -8,7 +8,7 @@ from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.config import config -from bitorch.layers.extensions import LayerRegistry, LayerImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry from bitorch.layers.qactivation import QActivation from bitorch.quantizations import Quantization diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 75cf56e..3329cf7 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -1,5 +1,5 @@ """Module containing the quantized linear layer""" -from typing import Union +from typing import Union, Dict, Any import torch from torch.nn import Linear @@ -8,7 +8,8 @@ from bitorch import RuntimeMode, runtime_mode_type from bitorch.quantizations import Quantization from .config import config -from .extensions.layer_implementation import LayerImplementation, LayerRegistry, DefaultImplementation +from .extensions.layer_implementation import DefaultImplementation +from .extensions import LayerRecipe, LayerImplementation, LayerRegistry from .qactivation import QActivation @@ -33,9 +34,37 @@ def __init__( **kwargs: keyword arguments for linear layer """ super().__init__(*args, **kwargs) # type: ignore - self.weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) + self.weight_quantization = config.get_quantization_function(weight_quantization or config.weight_quantization) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) + @staticmethod + def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: + """ + Gather all arguments that were used to create a QLinear layer with names. + Can be used to recreate a layer with identical arguments. + + Returns: + A dictionary with all arguments + """ + return { + "in_features": recipe.args[0], + "out_features": recipe.args[1], + "input_quantization": recipe.layer.input_quantization, + "gradient_cancellation_threshold": recipe.layer, + "weight_quantization": recipe.layer.weight_quantization, + "bias": recipe.get_by_position_or_key(5, "bias", True), + "device": recipe.get_by_position_or_key(6, "device", None), + "dtype": recipe.get_by_position_or_key(7, "dtype", None), + } + + @property + def input_quantization(self) -> Quantization: + return self.activation.activation_function + + @property + def gradient_cancellation_threshold(self) -> float: + return self.activation.gradient_cancellation_threshold + def forward(self, x: torch.Tensor) -> torch.Tensor: """Forwards x through the binary linear layer. @@ -46,7 +75,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: torch.Tensors: forwarded tensor """ - return linear(self.activation(x), self.weight_quantize(self.weight), self.bias) + return linear(self.activation(x), self.weight_quantization(self.weight), self.bias) q_linear_registry = LayerRegistry("QLinear") diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 8634e98..93213fb 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -6,7 +6,7 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset -from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct +from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, convert from bitorch.layers.extensions.layer_container import LayerContainer from bitorch.layers.qconv1d import q_conv1d_registry from bitorch.layers.qconv2d import q_conv2d_registry @@ -74,8 +74,4 @@ def initialize(self) -> None: nn.init.xavier_normal_(module.weight) def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": - modules_to_replace = list(self.modules()) - for registry in (q_linear_registry, q_conv1d_registry, q_conv2d_registry, q_conv3d_registry): - registry.convert_layers_to(new_mode, filter_=modules_to_replace, device=device, verbose=verbose) - self._model.to(device) - return self + return convert(self, new_mode, device, verbose) diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index 6988a52..37c0d83 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -14,7 +14,9 @@ class RuntimeMode(Enum): RAW = 0 DEFAULT = 1 - INFERENCE_AUTO = 2 + CPU = 2 + GPU = 4 + INFERENCE_AUTO = 8 def __add__(self, other: runtime_mode_type) -> runtime_mode_type: if self._to_int(self) == self._to_int(other): @@ -57,6 +59,8 @@ def from_string(level: str) -> "RuntimeMode": return { "raw": RuntimeMode.RAW, "default": RuntimeMode.DEFAULT, + "cpu": RuntimeMode.CPU, + "gpu": RuntimeMode.GPU, "inference_auto": RuntimeMode.INFERENCE_AUTO, }[level.lower()] diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 3dbd424..cab0ec3 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -20,6 +20,9 @@ import bitorch_inference_engine +bitorch_inference_engine.initialize() + + class QuantizedMLP(Model): def __init__(self, num_hidden_units_1=256, num_hidden_units_2=128): super().__init__(dataset=MNIST) diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 9aeae7c..f01a22a 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -4,12 +4,25 @@ import bitorch from bitorch import RuntimeMode -from bitorch.layers.extensions.layer_implementation import LayerImplementation, LayerRegistry, \ - CustomImplementation, DefaultImplementation, LayerRecipe +from bitorch.layers.extensions.layer_implementation import CustomImplementation, DefaultImplementation +from bitorch.layers.extensions import LayerRecipe, LayerImplementation, LayerRegistry from bitorch.layers.extensions.layer_container import LayerContainer TEST_MODE = RuntimeMode.INFERENCE_AUTO + +class TestLayerBase: + def __init__(self, s: str, val: int = 42) -> None: + self.s = s + self.val = val + + def do_something(self): + return f"{self.s}: {self.val} - made by {self.class_name()}" + + def class_name(self) -> str: + return self.__class__.__name__ + + test_registry = LayerRegistry("TestLayer") @@ -19,24 +32,16 @@ def __init__(self, *args): @TestLayerImplementation(RuntimeMode.DEFAULT) -class TestLayerDefaultMode(DefaultImplementation): - def __init__(self, s: str, val: int = 42) -> None: - self.s = s - self.val = val +class TestLayerDefaultMode(DefaultImplementation, TestLayerBase): + """Designate the TestLayerBase as the Default Mode""" + pass - def do_something(self): - return f"{self.s}: {self.val} - made by {self.class_name()}" - def class_name(self) -> str: - return self.__class__.__name__ +TestLayer = TestLayerDefaultMode @TestLayerImplementation(TEST_MODE) -class TestLayerCustomMode(CustomImplementation): - def __init__(self, s: str, val: int = 42) -> None: - self.s = s - self.val = val - +class TestLayerCustomMode(CustomImplementation, TestLayerBase): @classmethod def can_clone(cls, recipe: LayerRecipe) -> bool: # assume this test class can only clone layers with 'vals' lower than 100 @@ -54,9 +59,6 @@ def class_name(self) -> str: return self.__class__.__name__ -TestLayer = TestLayerDefaultMode - - @pytest.fixture(scope='function', autouse=True) def clean_environment(): test_registry.clear() @@ -92,7 +94,7 @@ def test_train_impl(): s = TestLayer("Hello World", val=21) assert s.val == 21 assert s.class_name() == "TestLayerCustomMode" - assert isinstance(s, TestLayerCustomMode.class_) + assert isinstance(s, TestLayerCustomMode) assert isinstance(s, LayerContainer) @@ -111,11 +113,11 @@ def test_clone(val, is_supported): s_recipe = test_registry.get_recipe_for(s) if is_supported: replacement = test_registry.get_replacement(TEST_MODE, s_recipe) - assert isinstance(replacement, TestLayerCustomMode.class_) # type: ignore + assert isinstance(replacement, TestLayerCustomMode) # type: ignore else: with pytest.raises(RuntimeError) as e_info: _ = test_registry.get_replacement(TEST_MODE, s_recipe) error_message = str(e_info.value) assert e_info.typename == "RuntimeError" - expected_key_strings = ["TestLayer", "layer implementation", str(TEST_MODE)] + expected_key_strings = ["TestLayer", "implementation", str(TEST_MODE)] assert all(key in error_message for key in expected_key_strings) diff --git a/tests/layers/test_qactivation.py b/tests/layers/test_qactivation.py index 3050a12..33ba071 100644 --- a/tests/layers/test_qactivation.py +++ b/tests/layers/test_qactivation.py @@ -12,16 +12,18 @@ @pytest.mark.parametrize("threshold", TEST_THRESHOLDS) -def test_qactivation(threshold): +def test_q_activation(threshold): input_quantization = config.get_quantization_function(config.input_quantization) - assert isinstance(activation._activation, type(input_quantization)) - assert isinstance(QActivation("sign")._activation, Sign) - assert isinstance(QActivation(Sign())._activation, Sign) + + assert isinstance(activation.activation_function, type(input_quantization)) + assert isinstance(QActivation("sign").activation_function, Sign) + assert isinstance(QActivation(Sign()).activation_function, Sign) + with pytest.raises(ValueError): QActivation("iNvAlIdNaMe") x = torch.Tensor(TEST_DATA).float().requires_grad_(True) - activation._gradient_cancellation_threshold = threshold + activation.gradient_cancellation_threshold = threshold y = activation(x) y.backward(x) diff --git a/tests/layers/test_qlinear.py b/tests/layers/test_qlinear.py index 86c5d34..158dd0b 100644 --- a/tests/layers/test_qlinear.py +++ b/tests/layers/test_qlinear.py @@ -19,7 +19,8 @@ @pytest.mark.parametrize("quantization", ["sign", Sign()]) def test_qlinear(input_values, quantization): layer = QLinear(2, 2, bias=False, weight_quantization=quantization, input_quantization=quantization) - assert isinstance(layer.weight_quantize, quantization_from_name("sign")) + assert isinstance(layer.weight_quantization, quantization_from_name("sign")) + assert isinstance(layer.input_quantization, quantization_from_name("sign")) test_weights = [[0.3, -1.4], [-0.3, 2.6]] diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 9d138a5..5cb3678 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -10,7 +10,8 @@ from bitorch import RuntimeMode from bitorch.datasets import MNIST from bitorch.layers import QConv2d, QLinear -from bitorch.layers.extensions.layer_implementation import LayerRecipe, CustomImplementation +from bitorch.layers.extensions.layer_implementation import CustomImplementation +from bitorch.layers.extensions import LayerRecipe from bitorch.layers.qconv1d import q_conv1d_registry from bitorch.layers.qconv2d import QConv2dBase, QConv2dImplementation, q_conv2d_registry from bitorch.layers.qconv3d import q_conv3d_registry From f25e055788f921d3be7fca01bb9574efa42b0584 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 15 Jun 2022 15:48:04 +0200 Subject: [PATCH 028/208] move registries into extra file --- bitorch/layers/__init__.py | 25 +++------ bitorch/layers/extensions/layer_recipe.py | 25 ++++++++- bitorch/layers/qconv1d.py | 24 ++------- bitorch/layers/qconv2d.py | 24 ++------- bitorch/layers/qconv3d.py | 24 ++------- bitorch/layers/qlinear.py | 33 ++++-------- bitorch/layers/register.py | 66 +++++++++++++++++++++++ bitorch/models/base.py | 5 +- tests/models/test_model_conversion.py | 8 +-- 9 files changed, 127 insertions(+), 107 deletions(-) create mode 100644 bitorch/layers/register.py diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 9043932..c15f588 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -3,11 +3,12 @@ and activations before forwarding them. These layers use the quantization functions specified in the quantization submodule. """ -from typing import List, TypeVar +from typing import TypeVar import torch from torch import nn +from bitorch import RuntimeMode from .debug_layers import ( InputGraphicalDebug, InputPrintDebug, @@ -18,11 +19,12 @@ from .extensions import CustomImplementation, LayerRegistry from .pact import Pact from .qactivation import QActivation -from .qconv1d import QConv1d, QConv1d_NoAct, q_conv1d_registry -from .qconv2d import QConv2d, QConv2d_NoAct, q_conv2d_registry -from .qconv3d import QConv3d, QConv3d_NoAct, q_conv3d_registry +from .qconv1d import QConv1d, QConv1d_NoAct +from .qconv2d import QConv2d, QConv2d_NoAct +from .qconv3d import QConv3d, QConv3d_NoAct from .qembedding import QEmbedding, QEmbeddingBag -from .qlinear import QLinear, QLinearBase, q_linear_registry +from .qlinear import QLinear, QLinearBase +from .register import all_layer_registries __all__ = [ "InputGraphicalDebug", "InputPrintDebug", "WeightGraphicalDebug", "WeightPrintDebug", @@ -31,17 +33,6 @@ "CustomImplementation", "convert" ] -from .. import RuntimeMode - - -def _get_layer_registries() -> List[LayerRegistry]: - return [ - q_conv1d_registry, - q_conv2d_registry, - q_conv3d_registry, - q_linear_registry, - ] - T = TypeVar("T", bound=nn.Module) @@ -59,7 +50,7 @@ def convert(module: T, new_mode: RuntimeMode, device: torch.device = None, verbo the converted module """ submodules = list(module.modules()) - for registry in _get_layer_registries(): + for registry in all_layer_registries(): registry.convert_layers_to(new_mode, only=submodules, device=device, verbose=verbose) module.to(device) return module diff --git a/bitorch/layers/extensions/layer_recipe.py b/bitorch/layers/extensions/layer_recipe.py index c5da665..f660885 100644 --- a/bitorch/layers/extensions/layer_recipe.py +++ b/bitorch/layers/extensions/layer_recipe.py @@ -16,7 +16,30 @@ class LayerRecipe: args: Tuple[Any, ...] kwargs: Dict[str, Any] - def get_by_position_or_key(self, pos: int, key: str, default: T) -> T: + def get_positional_arg(self, pos: int) -> Any: + """ + Get a positional argument from the stored args. + + Args: + pos: the position of the argument if given as a positional arg + + Returns: + the argument value retrieved + """ + return self.args[pos] + + def get_arg(self, pos: int, key: str, default: T) -> T: + """ + Get an argument from the stored args or kwargs. + + Args: + pos: the position of the argument if given as a positional arg + key: the name of the argument + default: the default value of the argument + + Returns: + the argument value retrieved + """ if len(self.args) > pos: return self.args[pos] return self.kwargs.get(key, default) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 4d112dd..c0cbb23 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -6,11 +6,12 @@ from torch.nn import Conv1d, init from torch.nn.functional import pad, conv1d -from bitorch import runtime_mode_type, RuntimeMode -from bitorch.layers.config import config -from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry -from bitorch.layers.qactivation import QActivation +from bitorch import RuntimeMode from bitorch.quantizations import Quantization +from .config import config +from .extensions import DefaultImplementation +from .qactivation import QActivation +from .register import QConv1dImplementation class QConv1d_NoAct(Conv1d): # noqa: N801 @@ -102,21 +103,6 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -q_conv1d_registry = LayerRegistry("QConv1d") - - -class QConv1dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv1d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ - def __init__(self, supports_modes: runtime_mode_type) -> None: - """ - Args: - supports_modes: RuntimeMode(s) that is/are supported by an implementation - """ - super().__init__(q_conv1d_registry, supports_modes) - - @QConv1dImplementation(RuntimeMode.DEFAULT) class QLinearDefaultImplementation(DefaultImplementation, QConv1dBase): """ diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index c70f500..88e6fb1 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -6,11 +6,12 @@ from torch.nn import Conv2d, init from torch.nn.functional import pad, conv2d -from bitorch import runtime_mode_type, RuntimeMode -from bitorch.layers.config import config -from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry -from bitorch.layers.qactivation import QActivation +from bitorch import RuntimeMode from bitorch.quantizations import Quantization +from .config import config +from .extensions import DefaultImplementation +from .qactivation import QActivation +from .register import QConv2dImplementation class QConv2d_NoAct(Conv2d): # type: ignore # noqa: N801 @@ -100,21 +101,6 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -q_conv2d_registry = LayerRegistry("QConv2d") - - -class QConv2dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv2d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ - def __init__(self, supports_modes: runtime_mode_type) -> None: - """ - Args: - supports_modes: RuntimeMode(s) that is/are supported by an implementation - """ - super().__init__(q_conv2d_registry, supports_modes) - - @QConv2dImplementation(RuntimeMode.DEFAULT) class QLinearDefaultImplementation(DefaultImplementation, QConv2dBase): """ diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index b81ed7c..7355e4a 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -6,11 +6,12 @@ from torch.nn import Conv3d, init from torch.nn.functional import pad, conv3d -from bitorch import runtime_mode_type, RuntimeMode -from bitorch.layers.config import config -from bitorch.layers.extensions import LayerImplementation, DefaultImplementation, LayerRegistry -from bitorch.layers.qactivation import QActivation +from bitorch import RuntimeMode from bitorch.quantizations import Quantization +from .config import config +from .extensions import DefaultImplementation +from .qactivation import QActivation +from .register import QConv3dImplementation class QConv3d_NoAct(Conv3d): # type: ignore # noqa: N801 @@ -100,21 +101,6 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -q_conv3d_registry = LayerRegistry("QConv3d") - - -class QConv3dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv3d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ - def __init__(self, supports_modes: runtime_mode_type) -> None: - """ - Args: - supports_modes: RuntimeMode(s) that is/are supported by an implementation - """ - super().__init__(q_conv3d_registry, supports_modes) - - @QConv3dImplementation(RuntimeMode.DEFAULT) class QLinearDefaultImplementation(DefaultImplementation, QConv3dBase): """ diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 3329cf7..3547598 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -5,12 +5,12 @@ from torch.nn import Linear from torch.nn.functional import linear -from bitorch import RuntimeMode, runtime_mode_type +from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions.layer_implementation import DefaultImplementation -from .extensions import LayerRecipe, LayerImplementation, LayerRegistry +from .extensions import LayerRecipe, DefaultImplementation from .qactivation import QActivation +from .register import QLinearImplementation class QLinearBase(Linear): @@ -47,14 +47,14 @@ def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: A dictionary with all arguments """ return { - "in_features": recipe.args[0], - "out_features": recipe.args[1], + "in_features": recipe.get_positional_arg(0), + "out_features": recipe.get_positional_arg(1), "input_quantization": recipe.layer.input_quantization, - "gradient_cancellation_threshold": recipe.layer, + "gradient_cancellation_threshold": recipe.layer.gradient_cancellation_threshold, "weight_quantization": recipe.layer.weight_quantization, - "bias": recipe.get_by_position_or_key(5, "bias", True), - "device": recipe.get_by_position_or_key(6, "device", None), - "dtype": recipe.get_by_position_or_key(7, "dtype", None), + "bias": recipe.get_arg(5, "bias", True), + "device": recipe.get_arg(6, "device", None), + "dtype": recipe.get_arg(7, "dtype", None), } @property @@ -78,21 +78,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return linear(self.activation(x), self.weight_quantization(self.weight), self.bias) -q_linear_registry = LayerRegistry("QLinear") - - -class QLinearImplementation(LayerImplementation): - """ - Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ - def __init__(self, supports_modes: runtime_mode_type) -> None: - """ - Args: - supports_modes: RuntimeMode(s) that is/are supported by an implementation - """ - super().__init__(q_linear_registry, supports_modes) - - @QLinearImplementation(RuntimeMode.DEFAULT) class QLinearDefaultImplementation(DefaultImplementation, QLinearBase): """ diff --git a/bitorch/layers/register.py b/bitorch/layers/register.py new file mode 100644 index 0000000..1fd8817 --- /dev/null +++ b/bitorch/layers/register.py @@ -0,0 +1,66 @@ +from typing import List + +from bitorch import runtime_mode_type +from bitorch.layers.extensions import LayerImplementation, LayerRegistry + +q_linear_registry = LayerRegistry("QLinear") +q_conv1d_registry = LayerRegistry("QConv1d") +q_conv2d_registry = LayerRegistry("QConv2d") +q_conv3d_registry = LayerRegistry("QConv3d") + + +def all_layer_registries() -> List[LayerRegistry]: + return [ + q_conv1d_registry, + q_conv2d_registry, + q_conv3d_registry, + q_linear_registry, + ] + + +class QLinearImplementation(LayerImplementation): + """ + Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_linear_registry, supports_modes) + + +class QConv1dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv1d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv1d_registry, supports_modes) + + +class QConv2dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv2d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv2d_registry, supports_modes) + + +class QConv3dImplementation(LayerImplementation): + """ + Decorator for :class:`QConv3d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. + """ + def __init__(self, supports_modes: runtime_mode_type) -> None: + """ + Args: + supports_modes: RuntimeMode(s) that is/are supported by an implementation + """ + super().__init__(q_conv3d_registry, supports_modes) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 93213fb..481513f 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -8,10 +8,7 @@ from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, convert from bitorch.layers.extensions.layer_container import LayerContainer -from bitorch.layers.qconv1d import q_conv1d_registry -from bitorch.layers.qconv2d import q_conv2d_registry -from bitorch.layers.qconv3d import q_conv3d_registry -from bitorch.layers.qlinear import q_linear_registry +from bitorch.layers.register import q_linear_registry, q_conv1d_registry, q_conv2d_registry, q_conv3d_registry class Model(nn.Module): diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 5cb3678..eedbdc4 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -12,10 +12,10 @@ from bitorch.layers import QConv2d, QLinear from bitorch.layers.extensions.layer_implementation import CustomImplementation from bitorch.layers.extensions import LayerRecipe -from bitorch.layers.qconv1d import q_conv1d_registry -from bitorch.layers.qconv2d import QConv2dBase, QConv2dImplementation, q_conv2d_registry -from bitorch.layers.qconv3d import q_conv3d_registry -from bitorch.layers.qlinear import QLinearImplementation, q_linear_registry, QLinearBase +from bitorch.layers.qconv2d import QConv2dBase +from bitorch.layers.qlinear import QLinearBase +from bitorch.layers.register import q_linear_registry, QLinearImplementation, q_conv1d_registry, q_conv2d_registry, \ + QConv2dImplementation, q_conv3d_registry from bitorch.models import Model TEST_MODE = RuntimeMode.INFERENCE_AUTO From 9d49a1cc7798429dfb945892e4ba70daac96b259 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 15 Jun 2022 17:00:02 +0200 Subject: [PATCH 029/208] rename bitwidth -> bit_width --- bitorch/layers/extensions/layer_registry.py | 2 +- bitorch/quantizations/approx_sign.py | 2 +- bitorch/quantizations/dorefa.py | 6 +++--- bitorch/quantizations/identity.py | 2 +- bitorch/quantizations/sign.py | 2 +- bitorch/quantizations/ste_heaviside.py | 2 +- bitorch/quantizations/swish_sign.py | 2 +- bitorch/runtime_mode.py | 8 ++++++++ tests/test_runtime_mode.py | 14 +++++++------- 9 files changed, 24 insertions(+), 16 deletions(-) diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index 7c571e3..a3302bf 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -81,7 +81,7 @@ def convert_layers_to( ) -> None: for recipe in list(self._instance_recipes): module = recipe.layer - if only is not None and module.layer_implementation not in only: + if only is not None and module.layer_implementation not in only and module not in only: continue assert isinstance(module, LayerContainer) if verbose: diff --git a/bitorch/quantizations/approx_sign.py b/bitorch/quantizations/approx_sign.py index e64718a..62c8840 100644 --- a/bitorch/quantizations/approx_sign.py +++ b/bitorch/quantizations/approx_sign.py @@ -53,7 +53,7 @@ class ApproxSign(Quantization): """Module for applying the sign function with approx sign in backward pass""" name = "approxsign" - bitwidth = 1 + bit_width = 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """Forwards the tensor through the approx sign function. diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index cab8212..44de62f 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -87,7 +87,7 @@ class InputDoReFa(Quantization): """ name = "inputdorefa" - bitwidth = config.dorefa_bits + bit_width = config.dorefa_bits def __init__(self, bits: Union[int, None] = None) -> None: """Initiates quantization bits. @@ -96,7 +96,7 @@ def __init__(self, bits: Union[int, None] = None) -> None: bits (int, optional): number of bits to quantize into. Defaults to None. """ super(InputDoReFa, self).__init__() - self.bitwidth = bits or config.dorefa_bits + self.bit_width = bits or config.dorefa_bits def quantize(self, x: torch.Tensor) -> torch.Tensor: """DoReFas the tensor to desired bit resolution. @@ -108,4 +108,4 @@ def quantize(self, x: torch.Tensor) -> torch.Tensor: torch.Tensor: DoReFaed tensor x """ - return InputDoReFaFunction.apply(x, self.bitwidth) + return InputDoReFaFunction.apply(x, self.bit_width) diff --git a/bitorch/quantizations/identity.py b/bitorch/quantizations/identity.py index 83af174..7d68d51 100644 --- a/bitorch/quantizations/identity.py +++ b/bitorch/quantizations/identity.py @@ -8,7 +8,7 @@ class Identity(Quantization): """Module that provides the identity function, which can be useful for certain training strategies""" name = "identity" - bitwidth = 32 + bit_width = 32 def quantize(self, x: torch.Tensor) -> torch.Tensor: """forwards the input tensor x without quantization. diff --git a/bitorch/quantizations/sign.py b/bitorch/quantizations/sign.py index 16818e2..ffa72d8 100644 --- a/bitorch/quantizations/sign.py +++ b/bitorch/quantizations/sign.py @@ -31,7 +31,7 @@ class Sign(Quantization): """Module for applying the sign function with straight through estimator in backward pass""" name = "sign" - bitwidth = 1 + bit_width = 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """Forwards the tensor through the sign function. diff --git a/bitorch/quantizations/ste_heaviside.py b/bitorch/quantizations/ste_heaviside.py index 655589a..052e355 100644 --- a/bitorch/quantizations/ste_heaviside.py +++ b/bitorch/quantizations/ste_heaviside.py @@ -29,7 +29,7 @@ class SteHeaviside(Quantization): """Module for applying the SteHeaviside quantization, using an ste in backward pass""" name = "steheaviside" - bitwidth = 1 + bit_width = 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """Forwards the tensor through the sign function. diff --git a/bitorch/quantizations/swish_sign.py b/bitorch/quantizations/swish_sign.py index c870c1e..196cf99 100644 --- a/bitorch/quantizations/swish_sign.py +++ b/bitorch/quantizations/swish_sign.py @@ -59,7 +59,7 @@ class SwishSign(Quantization): """Module for applying the SwishSign function""" name = "swishsign" - bitwidth = 1 + bit_width = 1 def __init__(self, beta: Union[float, None] = None) -> None: """Initializes gradient cancelation threshold. diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index 37c0d83..da0aa68 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -23,6 +23,14 @@ def __add__(self, other: runtime_mode_type) -> runtime_mode_type: return self return self._to_int(other) + self.value + @staticmethod + def available_values(): + return RuntimeMode.__members__.values() + + @staticmethod + def list_of_names(): + return RuntimeMode.__members__.keys() + @staticmethod def _max_val() -> int: return sum(map(lambda x: x.value, RuntimeMode.__members__.values())) diff --git a/tests/test_runtime_mode.py b/tests/test_runtime_mode.py index 02f763e..2b2cf0b 100644 --- a/tests/test_runtime_mode.py +++ b/tests/test_runtime_mode.py @@ -8,32 +8,32 @@ def test_mode_creation_from_name(): - for mode_str in RuntimeMode.__members__.keys(): + for mode_str in RuntimeMode.list_of_names(): assert isinstance(RuntimeMode.from_string(mode_str), RuntimeMode) def test_mode_supports_self(): - for mode in RuntimeMode.__members__.values(): + for mode in RuntimeMode.available_values(): assert mode.is_supported_by(mode) def test_mode_does_not_support_other_mode(): - for mode in RuntimeMode.__members__.values(): - for other_mode in RuntimeMode.__members__.values(): + for mode in RuntimeMode.available_values(): + for other_mode in RuntimeMode.available_values(): if mode == other_mode or mode == RuntimeMode.RAW or other_mode == RuntimeMode.RAW: continue assert not mode.is_supported_by(other_mode) def test_mode_self_addition(): - for mode in RuntimeMode.__members__.values(): + for mode in RuntimeMode.available_values(): same_mode_twice = mode + mode assert same_mode_twice == mode def test_mode_addition_supports_both(): - for mode in RuntimeMode.__members__.values(): - for other_mode in RuntimeMode.__members__.values(): + for mode in RuntimeMode.available_values(): + for other_mode in RuntimeMode.available_values(): if mode == other_mode: continue added_modes = mode + other_mode From 0c14f248e09f25945e8e13f63a28dfd710d318c7 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 15 Jun 2022 17:00:41 +0200 Subject: [PATCH 030/208] adapt changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0be92..313f264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Simple example script for MNIST -- Support for integration of bitorch's inference engine +- Support for integration of bitorch's inference engine for the following layers + - QLinear ### Changed From a3e54f6d81ea74ef1524153a809ebdcf9370e8ad Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 15 Jun 2022 17:06:18 +0200 Subject: [PATCH 031/208] fix types --- bitorch/runtime_mode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index da0aa68..c077bf0 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -1,7 +1,7 @@ from enum import Enum from functools import total_ordering from types import TracebackType -from typing import Union, Any, Optional, Type +from typing import Union, Any, Optional, Type, List import bitorch @@ -24,12 +24,12 @@ def __add__(self, other: runtime_mode_type) -> runtime_mode_type: return self._to_int(other) + self.value @staticmethod - def available_values(): - return RuntimeMode.__members__.values() + def available_values() -> List["RuntimeMode"]: + return RuntimeMode.__members__.values() # type:ignore @staticmethod - def list_of_names(): - return RuntimeMode.__members__.keys() + def list_of_names() -> List[str]: + return RuntimeMode.__members__.keys() # type:ignore @staticmethod def _max_val() -> int: From 3a0892888d9361da2c38264c4da4ff19e74ba72d Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 16 Jun 2022 08:16:31 +0200 Subject: [PATCH 032/208] test bit width of quantizations and add deprecation warning --- bitorch/quantizations/base.py | 10 ++++++++-- tests/quantizations/test_quantizations.py | 22 +++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/bitorch/quantizations/base.py b/bitorch/quantizations/base.py index 5db075e..ffbf499 100644 --- a/bitorch/quantizations/base.py +++ b/bitorch/quantizations/base.py @@ -5,6 +5,7 @@ from torch import nn from torch.autograd.function import Function from typing import Any +from warnings import warn class STE(Function): @@ -44,8 +45,13 @@ def backward(ctx: Any, output_gradient: torch.Tensor) -> torch.Tensor: class Quantization(nn.Module): """superclass for quantization modules""" - name = "None" - bit_width = -1 + name: str = "None" + bit_width: int = -1 + + @property + def bitwidth(self) -> int: + warn("Attribute 'bitwidth' is deprecated, use 'bit_width' instead.", DeprecationWarning, stacklevel=2) + return self.bit_width def quantize(self, x: torch.Tensor) -> torch.Tensor: """quantize the input tensor. It is recommended to use a torch.Function to also maniputlate backward behaiviour. See diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index 83b21e3..1aa97de 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -12,28 +12,29 @@ ) TEST_INPUT_DATA = [ - (Sign(), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + (Sign(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (ApproxSign(), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + (ApproxSign(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.4, 2.0, 1.4, 0.0, 0.0]), - (SteHeaviside(), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + (SteHeaviside(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (SwishSign(5.0), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + (SwishSign(5.0), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], [-0.03, -0.195, 1.562, 5.0, 1.562, -0.195, -0.03]), - (InputDoReFa(bits=2), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 1.0, 1.0], + (InputDoReFa(bits=2), 2, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (WeightDoReFa(bits=2), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + (WeightDoReFa(bits=2), 2, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - (InputDoReFa(bits=1), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], + (InputDoReFa(bits=1), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (WeightDoReFa(bits=1), [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + (WeightDoReFa(bits=1), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ] -@pytest.mark.parametrize("quantization, input_values, expected_output, expected_gradient_factors", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization, bits, input_values, expected_output, expected_gradient_factors", TEST_INPUT_DATA) def test_quantizations( quantization: Quantization, + bits: int, input_values: list, expected_output: list, expected_gradient_factors: list) -> None: @@ -45,6 +46,9 @@ def test_quantizations( y = quantization(x) assert torch.allclose(y, x_exp, atol=0.001) + assert quantization.bit_width == bits + with pytest.deprecated_call(): + assert quantization.bitwidth == bits y.backward(x) computed_gradient = x.grad.clone() From ccfea434ce5db5846215efb3cb49da2c4bae47ca Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 16 Jun 2022 09:34:38 +0200 Subject: [PATCH 033/208] name mixins as such --- bitorch/layers/__init__.py | 4 ++-- bitorch/layers/extensions/__init__.py | 2 +- bitorch/layers/extensions/layer_implementation.py | 4 ++-- bitorch/layers/extensions/layer_registration.py | 6 +++--- bitorch/layers/qconv1d.py | 4 ++-- bitorch/layers/qconv2d.py | 4 ++-- bitorch/layers/qconv3d.py | 4 ++-- bitorch/layers/qlinear.py | 4 ++-- tests/layers/test_layer_impl.py | 6 +++--- tests/models/test_model_conversion.py | 10 +++++----- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index c15f588..99888fa 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -16,7 +16,7 @@ WeightPrintDebug, ShapePrintDebug ) -from .extensions import CustomImplementation, LayerRegistry +from .extensions import CustomImplementationMixin, LayerRegistry from .pact import Pact from .qactivation import QActivation from .qconv1d import QConv1d, QConv1d_NoAct @@ -30,7 +30,7 @@ "InputGraphicalDebug", "InputPrintDebug", "WeightGraphicalDebug", "WeightPrintDebug", "ShapePrintDebug", "QActivation", "QConv1d", "QConv2d", "QConv3d", "QConv1d_NoAct", "QConv2d_NoAct", "QConv3d_NoAct", "QLinear", "QLinearBase", "QEmbedding", "QEmbeddingBag", "Pact", - "CustomImplementation", "convert" + "CustomImplementationMixin", "convert" ] diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py index 9f82267..377594c 100644 --- a/bitorch/layers/extensions/__init__.py +++ b/bitorch/layers/extensions/__init__.py @@ -3,7 +3,7 @@ """ from .layer_container import LayerContainer -from .layer_implementation import DefaultImplementation, CustomImplementation +from .layer_implementation import DefaultImplementationMixin, CustomImplementationMixin from .layer_registration import LayerImplementation from .layer_registry import LayerRegistry from .layer_recipe import LayerRecipe diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 2643cdc..64d1668 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -45,7 +45,7 @@ def create_clone_from(cls, recipe: "LayerRecipe") -> Any: raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") -class DefaultImplementation(BaseImplementation, ABC): +class DefaultImplementationMixin(BaseImplementation, ABC): """Defines the class interface of a default layer implementation of a certain layer type.""" @classmethod def is_default_implementation(cls) -> bool: @@ -60,7 +60,7 @@ def create_clone_from(cls, recipe: "LayerRecipe") -> Any: return cls(*recipe.args, **recipe.kwargs) -class CustomImplementation(BaseImplementation, ABC): +class CustomImplementationMixin(BaseImplementation, ABC): """Defines the class interface of a custom layer implementation of a certain layer type.""" @classmethod def is_default_implementation(cls) -> bool: diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index 5c0416e..d0da2ac 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -4,7 +4,7 @@ import bitorch from bitorch import runtime_mode_type, RuntimeMode from .layer_container import LayerContainer -from .layer_implementation import DefaultImplementation, BaseImplementation, CustomImplementation +from .layer_implementation import DefaultImplementationMixin, BaseImplementation, CustomImplementationMixin from .layer_recipe import LayerRecipe if TYPE_CHECKING: @@ -58,12 +58,12 @@ def _initialize(self, class_: Type[BaseImplementation]) -> Union["LayerImplement self.class_name = self.class_.__name__ self.registry.register(self) if self._supported_modes == RuntimeMode.DEFAULT: - assert issubclass(self.class_, DefaultImplementation), \ + assert issubclass(self.class_, DefaultImplementationMixin), \ f"{self.class_name} should be a subclass of DefaultLayerImplementation." # provide this wrapper return self else: - assert issubclass(self.class_, CustomImplementation), \ + assert issubclass(self.class_, CustomImplementationMixin), \ f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " \ f"implement the corresponding class methods)." # after we have registered custom implementations, we do not interfere anymore diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index c0cbb23..ad536f0 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -9,7 +9,7 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import DefaultImplementation +from .extensions import DefaultImplementationMixin from .qactivation import QActivation from .register import QConv1dImplementation @@ -104,7 +104,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv1dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementation, QConv1dBase): +class QLinearDefaultImplementation(DefaultImplementationMixin, QConv1dBase): """ This class defines the default implementation of a QConv1d layer (which is actually implemented by QConv1dBase). diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 88e6fb1..b54da34 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -9,7 +9,7 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import DefaultImplementation +from .extensions import DefaultImplementationMixin from .qactivation import QActivation from .register import QConv2dImplementation @@ -102,7 +102,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv2dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementation, QConv2dBase): +class QLinearDefaultImplementation(DefaultImplementationMixin, QConv2dBase): """ This class defines the default implementation of a QConv2d layer (which is actually implemented by QConv2dBase). diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 7355e4a..db8dc90 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -9,7 +9,7 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import DefaultImplementation +from .extensions import DefaultImplementationMixin from .qactivation import QActivation from .register import QConv3dImplementation @@ -102,7 +102,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv3dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementation, QConv3dBase): +class QLinearDefaultImplementation(DefaultImplementationMixin, QConv3dBase): """ This class defines the default implementation of a QConv3d layer (which is actually implemented by QConv3dBase). diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 3547598..36f1c83 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -8,7 +8,7 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import LayerRecipe, DefaultImplementation +from .extensions import LayerRecipe, DefaultImplementationMixin from .qactivation import QActivation from .register import QLinearImplementation @@ -79,7 +79,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: @QLinearImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementation, QLinearBase): +class QLinearDefaultImplementation(DefaultImplementationMixin, QLinearBase): """ This class defines the default implementation of a QLinear layer (which is actually implemented by QLinearBase). diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index f01a22a..704b026 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -4,7 +4,7 @@ import bitorch from bitorch import RuntimeMode -from bitorch.layers.extensions.layer_implementation import CustomImplementation, DefaultImplementation +from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin, DefaultImplementationMixin from bitorch.layers.extensions import LayerRecipe, LayerImplementation, LayerRegistry from bitorch.layers.extensions.layer_container import LayerContainer @@ -32,7 +32,7 @@ def __init__(self, *args): @TestLayerImplementation(RuntimeMode.DEFAULT) -class TestLayerDefaultMode(DefaultImplementation, TestLayerBase): +class TestLayerDefaultMode(DefaultImplementationMixin, TestLayerBase): """Designate the TestLayerBase as the Default Mode""" pass @@ -41,7 +41,7 @@ class TestLayerDefaultMode(DefaultImplementation, TestLayerBase): @TestLayerImplementation(TEST_MODE) -class TestLayerCustomMode(CustomImplementation, TestLayerBase): +class TestLayerCustomMode(CustomImplementationMixin, TestLayerBase): @classmethod def can_clone(cls, recipe: LayerRecipe) -> bool: # assume this test class can only clone layers with 'vals' lower than 100 diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index eedbdc4..fe4c53b 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -10,7 +10,7 @@ from bitorch import RuntimeMode from bitorch.datasets import MNIST from bitorch.layers import QConv2d, QLinear -from bitorch.layers.extensions.layer_implementation import CustomImplementation +from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin from bitorch.layers.extensions import LayerRecipe from bitorch.layers.qconv2d import QConv2dBase from bitorch.layers.qlinear import QLinearBase @@ -51,7 +51,7 @@ def get_decorated_impls(): reset() @QLinearImplementation(TEST_MODE) - class QLinearTestImpl(CustomImplementation, nn.Module): + class QLinearTestImpl(CustomImplementationMixin, nn.Module): def __init__(self, *args, **kwargs): super().__init__() with bitorch.pause_wrapping(): @@ -73,7 +73,7 @@ def create_clone_from(cls, recipe: LayerRecipe) -> Any: return new_layer @QConv2dImplementation(TEST_MODE) - class QConv2dTestImpl(CustomImplementation, nn.Module): + class QConv2dTestImpl(CustomImplementationMixin, nn.Module): def __init__(self, *args, **kwargs): super().__init__() with bitorch.pause_wrapping(): @@ -102,7 +102,7 @@ def get_subclassed_impls(): reset() @QLinearImplementation(TEST_MODE) - class QLinearTestImpl(CustomImplementation, QLinearBase): + class QLinearTestImpl(CustomImplementationMixin, QLinearBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.is_test_implementation = True @@ -119,7 +119,7 @@ def create_clone_from(cls, recipe: LayerRecipe) -> Any: return new_layer @QConv2dImplementation(TEST_MODE) - class QConv2dTestImpl(CustomImplementation, QConv2dBase): + class QConv2dTestImpl(CustomImplementationMixin, QConv2dBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.is_test_implementation = True From dacb79dbb67fac5651e3519bc41e6a73bd37786b Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 16 Jun 2022 15:20:35 +0200 Subject: [PATCH 034/208] add error messages if implementations do not support a given layer --- .../layers/extensions/layer_implementation.py | 12 +++--- .../layers/extensions/layer_registration.py | 11 ++--- bitorch/layers/extensions/layer_registry.py | 42 ++++++++++++++++--- bitorch/layers/qconv1d.py | 5 +-- bitorch/layers/qconv2d.py | 5 +-- bitorch/layers/qconv3d.py | 5 +-- bitorch/layers/qlinear.py | 5 +-- bitorch/runtime_mode.py | 10 +++++ tests/layers/test_layer_impl.py | 7 ++-- tests/models/test_model_conversion.py | 16 +++---- 10 files changed, 75 insertions(+), 43 deletions(-) diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 64d1668..5d9caf6 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Any, TYPE_CHECKING +from typing import Any, Tuple, TYPE_CHECKING if TYPE_CHECKING: from . import LayerRecipe @@ -19,7 +19,7 @@ def is_default_implementation(cls) -> bool: raise NotImplementedError("Should be implemented by subclass.") @classmethod - def can_clone(cls, recipe: "LayerRecipe") -> bool: + def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: """ Returns whether this layer class supports the implementation of a given layer recipe. @@ -27,7 +27,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> bool: recipe (LayerRecipe): the layer which should be checked for cloning Returns: - bool: Whether the layer can be cloned or not + Whether the layer can be cloned or not and an info message if it can not be cloned """ raise NotImplementedError("A custom layer should implement their own compatibility check.") @@ -52,8 +52,8 @@ def is_default_implementation(cls) -> bool: return True @classmethod - def can_clone(cls, recipe: "LayerRecipe") -> bool: - return True + def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: + return True, "" @classmethod def create_clone_from(cls, recipe: "LayerRecipe") -> Any: @@ -67,7 +67,7 @@ def is_default_implementation(cls) -> bool: return False @classmethod - def can_clone(cls, recipe: "LayerRecipe") -> bool: + def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index d0da2ac..dc51fa9 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -1,8 +1,9 @@ from abc import ABC -from typing import Any, Type, Union, TYPE_CHECKING +from typing import Any, Type, Union, Tuple, TYPE_CHECKING import bitorch from bitorch import runtime_mode_type, RuntimeMode + from .layer_container import LayerContainer from .layer_implementation import DefaultImplementationMixin, BaseImplementation, CustomImplementationMixin from .layer_recipe import LayerRecipe @@ -81,16 +82,16 @@ def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerConta def supports_mode(self, mode: RuntimeMode) -> bool: """ - Check whether the stored layer implementation supports a given RuntimeMode. + Check whether this layer implementation supports a given RuntimeMode. Args: - mode: + mode: the runtime mode that should be supported Returns: - + True if the given mode is supported, False otherwise """ return mode.is_supported_by(self._supported_modes) - def can_create_clone_from(self, recipe: LayerRecipe) -> bool: + def can_create_clone_from(self, recipe: LayerRecipe) -> Tuple[bool, str]: return self.class_.can_clone(recipe) def get_replacement(self, recipe: LayerRecipe) -> Any: diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index a3302bf..3d9fdfd 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -1,9 +1,9 @@ from typing import Set, Any, Optional, Iterable -import torch - import bitorch +import torch from bitorch import RuntimeMode + from .layer_container import LayerContainer from .layer_recipe import LayerRecipe from .layer_registration import LayerImplementation @@ -44,24 +44,56 @@ def __contains__(self, item: Any) -> bool: return item.__class__ in map(lambda x: x.class_, self.layer_implementations) def register(self, layer: LayerImplementation) -> None: + """ + Register a layer implementaiton in this registry. + + Args: + layer: the layer to be registered + """ self.layer_implementations.add(layer) def get_layer( self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None ) -> LayerImplementation: + """ + Get a layer implementaiton compatible to the given mode and recipe. + + If no recipe is given, only compatibility with the mode is checked. + If no mode is given, the current bitorch mode is used. + + Args: + mode: mode that the layer implementation should support + recipe: recipe that the layer implementation should be able to copy + + Returns: + a LayerImplementation compatible with the given mode and recipe (if available) + """ if mode is None: mode = bitorch.mode available_layers = [] + unavailable_layers = [] + for implementation in self.layer_implementations: if not implementation.supports_mode(mode): continue - if recipe and not implementation.can_create_clone_from(recipe): - continue + if recipe: + return_tuple = implementation.can_create_clone_from(recipe) + if not isinstance(return_tuple, tuple) and len(return_tuple) == 2: + raise RuntimeError(f"{implementation.__class__} returned non-tuple on 'can_create_clone_from'.") + can_be_used, message = return_tuple + if not can_be_used: + unavailable_layers.append(f" {implementation.__class__} unavailable because: {message}") + continue available_layers.append(implementation) + if len(available_layers) > 1: RuntimeWarning(f"Multiple layer implementations available for '{self.name}' available (mode='{mode}').") if len(available_layers) == 0: - raise RuntimeError(f"No implementation for '{self.name}' available (mode='{mode}').") + base_error = f"No implementations for '{self.name}' available (mode='{mode}')." + if len(unavailable_layers) > 0: + raise RuntimeError("\n".join([base_error] + unavailable_layers)) + else: + raise RuntimeError(base_error) return available_layers[0] def clear(self) -> None: diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index ad536f0..ae9febe 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -104,13 +104,10 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv1dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementationMixin, QConv1dBase): +class QConv1d(DefaultImplementationMixin, QConv1dBase): """ This class defines the default implementation of a QConv1d layer (which is actually implemented by QConv1dBase). To implement a custom QConv1d implementation use QConv1dBase as a super class instead. """ pass - - -QConv1d = QLinearDefaultImplementation diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index b54da34..31e5770 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -102,13 +102,10 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv2dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementationMixin, QConv2dBase): +class QConv2d(DefaultImplementationMixin, QConv2dBase): """ This class defines the default implementation of a QConv2d layer (which is actually implemented by QConv2dBase). To implement a custom QConv2d implementation use QConv2dBase as a super class instead. """ pass - - -QConv2d = QLinearDefaultImplementation diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index db8dc90..aaa582a 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -102,13 +102,10 @@ def forward(self, input_tensor: Tensor) -> Tensor: @QConv3dImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementationMixin, QConv3dBase): +class QConv3d(DefaultImplementationMixin, QConv3dBase): """ This class defines the default implementation of a QConv3d layer (which is actually implemented by QConv3dBase). To implement a custom QConv3d implementation use QConv3dBase as a super class instead. """ pass - - -QConv3d = QLinearDefaultImplementation diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 36f1c83..1a23174 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -79,13 +79,10 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: @QLinearImplementation(RuntimeMode.DEFAULT) -class QLinearDefaultImplementation(DefaultImplementationMixin, QLinearBase): +class QLinear(DefaultImplementationMixin, QLinearBase): """ This class defines the default implementation of a QLinear layer (which is actually implemented by QLinearBase). To implement a custom QLinear implementation use QLinearBase as a super class instead. """ pass - - -QLinear = QLinearDefaultImplementation diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index c077bf0..457be62 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -12,6 +12,16 @@ @total_ordering class RuntimeMode(Enum): + """ + Enum for BITorch modes: + + - DEFAULT: use the default implementation of all layers + - CPU: use layer implementations for inference on CPU + - GPU: use layer implementations for inference on GPU + - INFERENCE_AUTO: use an automatic layer that uses the fastest implementation available (not recommended) + - RAW: while in this mode, new layers are created as the default implementation BUT without wrapping, so they can + not be switched to other layers later on (it does not influence already wrapped layers) + """ RAW = 0 DEFAULT = 1 CPU = 2 diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 704b026..e3eedd5 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -46,7 +46,7 @@ class TestLayerCustomMode(CustomImplementationMixin, TestLayerBase): def can_clone(cls, recipe: LayerRecipe) -> bool: # assume this test class can only clone layers with 'vals' lower than 100 val = recipe.kwargs.get("val", recipe.args[2] if 2 < len(recipe.args) else None) - return val < 100 + return val < 100, "val needs to be smaller than 100" @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: @@ -119,5 +119,6 @@ def test_clone(val, is_supported): _ = test_registry.get_replacement(TEST_MODE, s_recipe) error_message = str(e_info.value) assert e_info.typename == "RuntimeError" - expected_key_strings = ["TestLayer", "implementation", str(TEST_MODE)] - assert all(key in error_message for key in expected_key_strings) + expected_key_strings = ["TestLayer", "implementation", str(TEST_MODE), "val", "100"] + for key in expected_key_strings: + assert key in error_message diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index fe4c53b..5fa967e 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Tuple import pytest import torch @@ -62,8 +62,8 @@ def forward(self, x): return self._layer(x) @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: - return True + def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: + return True, "" @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: @@ -84,8 +84,8 @@ def forward(self, x): return self._layer(x) @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: - return True + def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: + return True, "" @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: @@ -109,7 +109,7 @@ def __init__(self, *args, **kwargs): @classmethod def can_clone(cls, recipe: LayerRecipe) -> bool: - return True + return True, "" @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: @@ -125,8 +125,8 @@ def __init__(self, *args, **kwargs): self.is_test_implementation = True @classmethod - def can_clone(cls, recipe: LayerRecipe) -> bool: - return True + def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: + return True, "" @classmethod def create_clone_from(cls, recipe: LayerRecipe) -> Any: From e1d68e654ccb29b02900c9814b82788703b5d47c Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Thu, 16 Jun 2022 16:50:36 +0200 Subject: [PATCH 035/208] changes to metric computation --- .../pytorch_lightning/image_classification.py | 10 +++---- .../utils/lightning_model.py | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 41e8089..f57ef24 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -147,11 +147,11 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) total_flops = stats["#speed up flops (app.)"][""] logger.info("Approximated mflops: " + str(total_flops / 1e6)) - # for logger in loggers: - # logger.log_dict({ - # "mflops": total_flops / 1e6, - # "size in MB": total_size / 1e6 / 8.0, - # }) + if WANDB_AVAILABLE and args.wandb_log: + wandb.config.update({ + "mflops": total_flops / 1e6, + "size in MB": total_size / 1e6 / 8.0, + }) trainer.fit( model_wrapped, diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index f6a3149..3b0aae0 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -40,10 +40,10 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore self.train_accuracy_top1(y_hat, y_train) self.train_accuracy_top5(y_hat, y_train) self.log_dict({ - "metrics/train-top1-accuracy": self.accuracy_top1(y_hat, y_train), - "metrics/train-top5-accuracy": self.accuracy_top1(y_hat, y_train), - }, prog_bar=True) - self.log("loss/train", loss) + "metrics/train-top1-accuracy": self.train_accuracy_top1, + "metrics/train-top5-accuracy": self.train_accuracy_top5, + }, prog_bar=True, on_step=True, on_epoch=False) + self.log("loss/train", loss, on_step=True, on_epoch=False) return loss def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: ignore @@ -52,19 +52,25 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: y_hat = self.model(x_test) loss = self.loss_function(y_hat, y_test) + self.accuracy_top1(y_hat, y_test) + self.accuracy_top5(y_hat, y_test) + metrics_dict = { - "metrics/test-top1-accuracy": self.accuracy_top1(y_hat, y_test), - "metrics/test-top5-accuracy": self.accuracy_top5(y_hat, y_test), + "metrics/test-top1-accuracy": self.accuracy_top1, + "metrics/test-top5-accuracy": self.accuracy_top5, "loss/test": loss, } if self.add_f1_prec_recall: + self.f1(y_hat, y_test) + self.prec(y_hat, y_test) + self.recall(y_hat, y_test) metrics_dict.update({ - "metrics/f1": self.f1(y_hat, y_test), - "metrics/precision": self.prec(y_hat, y_test), - "metrics/recall": self.recall(y_hat, y_test), + "metrics/f1": self.f1, + "metrics/precision": self.prec, + "metrics/recall": self.recall, }) - self.log_dict(metrics_dict, prog_bar=True) + self.log_dict(metrics_dict, prog_bar=True, on_step=True, on_epoch=True) return loss From 8a86319b2b7c19fd0f00d5c13f041e950824c6e5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 16 Jun 2022 17:25:13 +0200 Subject: [PATCH 036/208] pass device to layer cloning --- bitorch/layers/extensions/layer_implementation.py | 8 +++++--- bitorch/layers/extensions/layer_registration.py | 5 +++-- bitorch/layers/extensions/layer_registry.py | 10 ++++++---- tests/layers/test_layer_impl.py | 3 ++- tests/models/test_model_conversion.py | 8 ++++---- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 5d9caf6..741f18b 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,6 +1,8 @@ from abc import ABC from typing import Any, Tuple, TYPE_CHECKING +import torch + if TYPE_CHECKING: from . import LayerRecipe @@ -32,7 +34,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe") -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: """ Create a new layer based on a given layer recipe (can be expected to be from the default category). @@ -56,7 +58,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: "LayerRecipe") -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: return cls(*recipe.args, **recipe.kwargs) @@ -71,5 +73,5 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe") -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index dc51fa9..2243962 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -2,6 +2,7 @@ from typing import Any, Type, Union, Tuple, TYPE_CHECKING import bitorch +import torch from bitorch import runtime_mode_type, RuntimeMode from .layer_container import LayerContainer @@ -94,8 +95,8 @@ def supports_mode(self, mode: RuntimeMode) -> bool: def can_create_clone_from(self, recipe: LayerRecipe) -> Tuple[bool, str]: return self.class_.can_clone(recipe) - def get_replacement(self, recipe: LayerRecipe) -> Any: - return self.class_.create_clone_from(recipe) + def get_replacement(self, recipe: LayerRecipe, device: torch.device) -> Any: + return self.class_.create_clone_from(recipe, device) def is_default(self) -> bool: return self.class_.is_default_implementation() diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index 3d9fdfd..61c5101 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -31,9 +31,9 @@ def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: return None return next(filter(lambda x: x.layer == layer, self._instance_recipes)) - def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe) -> Any: + def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe, device: torch.device) -> Any: layer = self.get_layer(mode, recipe) - return layer.get_replacement(recipe) + return layer.get_replacement(recipe, device) def add_recipe(self, new_recipe: LayerRecipe) -> None: if self.is_replacing: @@ -117,7 +117,9 @@ def convert_layers_to( continue assert isinstance(module, LayerContainer) if verbose: - print("Converting", module) - replacement_module = self.get_replacement(new_mode, recipe) + print("| Replacing layer in", module) + replacement_module = self.get_replacement(new_mode, recipe, device) replacement_module.to(device) + if verbose: + print("- with:", replacement_module) module.replace_layer_implementation(replacement_module) diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index e3eedd5..6add873 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -3,6 +3,7 @@ import pytest import bitorch +import torch from bitorch import RuntimeMode from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin, DefaultImplementationMixin from bitorch.layers.extensions import LayerRecipe, LayerImplementation, LayerRegistry @@ -49,7 +50,7 @@ def can_clone(cls, recipe: LayerRecipe) -> bool: return val < 100, "val needs to be smaller than 100" @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: return cls(recipe.layer.s, recipe.layer.val) def do_something(self): diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 5fa967e..a209364 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -66,7 +66,7 @@ def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer = cls(*recipe.args, **recipe.kwargs) new_layer._layer.weight = recipe.layer.weight new_layer._layer.bias = recipe.layer.bias @@ -88,7 +88,7 @@ def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer = cls(*recipe.args, **recipe.kwargs) new_layer._layer.weight = recipe.layer.weight new_layer._layer.bias = recipe.layer.bias @@ -112,7 +112,7 @@ def can_clone(cls, recipe: LayerRecipe) -> bool: return True, "" @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer = cls(*recipe.args, **recipe.kwargs) new_layer.weight = recipe.layer.weight new_layer.bias = recipe.layer.bias @@ -129,7 +129,7 @@ def can_clone(cls, recipe: LayerRecipe) -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: LayerRecipe) -> Any: + def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer = cls(*recipe.args, **recipe.kwargs) new_layer.weight = recipe.layer.weight new_layer.bias = recipe.layer.bias From 78594e8b02068bc74804afa17f391120f98e1c94 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Thu, 16 Jun 2022 17:55:31 +0200 Subject: [PATCH 037/208] set logger to include test and train acc --- examples/pytorch_lightning/utils/lightning_model.py | 2 +- examples/pytorch_lightning/utils/log.py | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 3b0aae0..0f3f806 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -70,7 +70,7 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: "metrics/precision": self.prec, "metrics/recall": self.recall, }) - self.log_dict(metrics_dict, prog_bar=True, on_step=True, on_epoch=True) + self.log_dict(metrics_dict, prog_bar=True, on_step=False, on_epoch=True) return loss diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index e5fe308..8800f9c 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -116,10 +116,6 @@ def on_train_batch_end( def _replace_metric_key(metric_key: str) -> str: remove_strings = [ "metrics/", - "/train", - "train-", - "/test", - "test-", ] for s in remove_strings: metric_key = metric_key.replace(s, "") @@ -128,16 +124,9 @@ def _replace_metric_key(metric_key: str) -> str: @staticmethod def _format_metric_string(metrics_dict: Dict[str, Union[int, str]], train: bool = True) -> str: metric_list = [] - skip_keys = {"v_num"} - if train: - skip_keys.add("test-top1-acc") - skip_keys.add("test-top5-acc") - else: - skip_keys.add("train-top1-acc") - skip_keys.add("train-top5-acc") for key, value in metrics_dict.items(): - if key in skip_keys: + if key == "v_num" or "loss" in key: continue key = CommandLineLogger._replace_metric_key(key) try: From bcc2f5c157f81897696441b5be974f90f49ed3b8 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 17 Jun 2022 13:48:30 +0200 Subject: [PATCH 038/208] add device for layer conversion --- bitorch/layers/extensions/layer_implementation.py | 9 +++++---- bitorch/layers/extensions/layer_registration.py | 2 +- bitorch/layers/extensions/layer_registry.py | 4 ++-- examples/mnist/train_mnist.py | 1 + 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 741f18b..a27cae8 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -34,12 +34,13 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: """ Create a new layer based on a given layer recipe (can be expected to be from the default category). Args: - recipe (LayerRecipe): the layer which should be cloned + recipe: the layer which should be cloned + device: the device on which the layer is going to be run Returns: A clone of the LayerRecipe in the current class implementation @@ -58,7 +59,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: return cls(*recipe.args, **recipe.kwargs) @@ -73,5 +74,5 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index 2243962..2afdcdd 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -95,7 +95,7 @@ def supports_mode(self, mode: RuntimeMode) -> bool: def can_create_clone_from(self, recipe: LayerRecipe) -> Tuple[bool, str]: return self.class_.can_clone(recipe) - def get_replacement(self, recipe: LayerRecipe, device: torch.device) -> Any: + def get_replacement(self, recipe: LayerRecipe, device: torch.device = None) -> Any: return self.class_.create_clone_from(recipe, device) def is_default(self) -> bool: diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index 61c5101..942dece 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -31,7 +31,7 @@ def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: return None return next(filter(lambda x: x.layer == layer, self._instance_recipes)) - def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe, device: torch.device) -> Any: + def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe, device: torch.device = None) -> Any: layer = self.get_layer(mode, recipe) return layer.get_replacement(recipe, device) @@ -56,7 +56,7 @@ def get_layer( self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None ) -> LayerImplementation: """ - Get a layer implementaiton compatible to the given mode and recipe. + Get a layer implementation compatible to the given mode and recipe. If no recipe is given, only compatibility with the mode is checked. If no mode is given, the current bitorch mode is used. diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index cab0ec3..95606c8 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -45,6 +45,7 @@ def forward(self, x): x = self._model.bn1(x) x = self._model.fc2(x) + x = x.to(dtype=torch.float32) # todo: fix in inference engine x = self._model.act2(x) x = self._model.bn2(x) From 8b24325ae2bc120d43b564166618f244822c34b6 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 17 Jun 2022 17:41:47 +0200 Subject: [PATCH 039/208] add docs and new function --- bitorch/layers/__init__.py | 1 + bitorch/layers/register.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 99888fa..21b82ff 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -40,6 +40,7 @@ def convert(module: T, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> T: """ Convert the given module to a new bitorch RuntimeMode. Needs to have custom implementations installed. + Args: module: the module to be converted new_mode: the new mode for the module diff --git a/bitorch/layers/register.py b/bitorch/layers/register.py index 1fd8817..5900bd4 100644 --- a/bitorch/layers/register.py +++ b/bitorch/layers/register.py @@ -1,6 +1,8 @@ -from typing import List +from typing import List, Iterable, Any, Optional -from bitorch import runtime_mode_type +import torch + +from bitorch import runtime_mode_type, RuntimeMode from bitorch.layers.extensions import LayerImplementation, LayerRegistry q_linear_registry = LayerRegistry("QLinear") @@ -10,6 +12,12 @@ def all_layer_registries() -> List[LayerRegistry]: + """ + Return all layer registries (one for each layer type: QLinear, QConv[1-3]d). + + Returns: + A list of all layer registries. + """ return [ q_conv1d_registry, q_conv2d_registry, @@ -18,6 +26,24 @@ def all_layer_registries() -> List[LayerRegistry]: ] +def convert_layers_to( + new_mode: RuntimeMode, + only: Optional[Iterable[Any]] = None, + device: torch.device = None, + verbose: bool = False +) -> None: + """ + Convert all wrapped layers (or a given subset of them) to a new mode. + Args: + new_mode: the new RuntimeMode + only: optional white"list" (Iterable) of layers or wrapped layers which should be converted + device: the new device for the layers + verbose: whether to print which layers are being converted + """ + for registry in all_layer_registries(): + registry.convert_layers_to(new_mode, only, device, verbose) + + class QLinearImplementation(LayerImplementation): """ Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. From e4654f56d729af1c98f99c16bfd700d6a5d65d57 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 20 Jun 2022 10:32:29 +0200 Subject: [PATCH 040/208] use normal module instead of bitorch model for example --- examples/mnist/train_mnist.py | 41 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 95606c8..52a6110 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -15,41 +15,40 @@ import bitorch.layers as qnn from bitorch import datasets as bitorch_datasets, RuntimeMode -from bitorch.datasets import MNIST -from bitorch.models import Model +from bitorch.layers import convert import bitorch_inference_engine bitorch_inference_engine.initialize() -class QuantizedMLP(Model): +class QuantizedMLP(nn.Module): def __init__(self, num_hidden_units_1=256, num_hidden_units_2=128): - super().__init__(dataset=MNIST) - self._model.flatten = nn.Flatten() - self._model.fc1 = nn.Linear(784, num_hidden_units_1) - self._model.act1 = nn.PReLU() - self._model.bn1 = nn.BatchNorm1d(num_hidden_units_1) + super().__init__() + self.flatten = nn.Flatten() + self.fc1 = nn.Linear(784, num_hidden_units_1) + self.act1 = nn.PReLU() + self.bn1 = nn.BatchNorm1d(num_hidden_units_1) - self._model.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2, bias=False) - self._model.act2 = nn.PReLU() - self._model.bn2 = nn.BatchNorm1d(num_hidden_units_2) + self.fc2 = qnn.QLinear(num_hidden_units_1, num_hidden_units_2, bias=False) + self.act2 = nn.PReLU() + self.bn2 = nn.BatchNorm1d(num_hidden_units_2) - self._model.fc3 = nn.Linear(num_hidden_units_2, 10) + self.fc3 = nn.Linear(num_hidden_units_2, 10) def forward(self, x): - x = self._model.flatten(x) + x = self.flatten(x) - x = self._model.fc1(x) - x = self._model.act1(x) - x = self._model.bn1(x) + x = self.fc1(x) + x = self.act1(x) + x = self.bn1(x) - x = self._model.fc2(x) + x = self.fc2(x) x = x.to(dtype=torch.float32) # todo: fix in inference engine - x = self._model.act2(x) - x = self._model.bn2(x) + x = self.act2(x) + x = self.bn2(x) - x = self._model.fc3(x) + x = self.fc3(x) output = F.log_softmax(x, dim=1) return output @@ -176,7 +175,7 @@ def main(): test(model, device, test_loader) scheduler.step() - inference_model = model.convert(RuntimeMode.INFERENCE_AUTO, device=device, verbose=True) + inference_model = convert(model, RuntimeMode.INFERENCE_AUTO, device=device, verbose=True) test(inference_model, device, test_loader) if args.save_model: From c7701e0d666623386187d7cf08e5e5633cd7e00e Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 20 Jun 2022 10:55:36 +0200 Subject: [PATCH 041/208] update docs for qlinear kwargs function --- bitorch/layers/qlinear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 1a23174..721fc29 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -40,11 +40,11 @@ def __init__( @staticmethod def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: """ - Gather all arguments that were used to create a QLinear layer with names. + Gather all arguments that were used to create a QLinear layer with argument names. Can be used to recreate a layer with identical arguments. Returns: - A dictionary with all arguments + A dictionary with all arguments (key is the argument name as a string even for positional arguments) """ return { "in_features": recipe.get_positional_arg(0), From 76a5026530c649aacf0b5d80d05b89225b7f9a07 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 21 Jun 2022 10:35:23 +0200 Subject: [PATCH 042/208] rename some functions and classes in tests --- bitorch/layers/extensions/__init__.py | 2 +- bitorch/layers/extensions/layer_container.py | 25 ++++++++-- bitorch/layers/extensions/layer_recipe.py | 4 +- .../layers/extensions/layer_registration.py | 6 +-- bitorch/layers/qconv2d.py | 36 +++++++++++++- tests/layers/test_layer_impl.py | 47 +++++++++---------- tests/layers/test_qconv.py | 19 ++++++++ tests/layers/test_qlinear.py | 39 +++++++++++++-- tests/layers/test_switchable_layer.py | 28 +++++++---- 9 files changed, 157 insertions(+), 49 deletions(-) diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py index 377594c..0e0731f 100644 --- a/bitorch/layers/extensions/__init__.py +++ b/bitorch/layers/extensions/__init__.py @@ -4,6 +4,6 @@ from .layer_container import LayerContainer from .layer_implementation import DefaultImplementationMixin, CustomImplementationMixin +from .layer_recipe import LayerRecipe from .layer_registration import LayerImplementation from .layer_registry import LayerRegistry -from .layer_recipe import LayerRecipe diff --git a/bitorch/layers/extensions/layer_container.py b/bitorch/layers/extensions/layer_container.py index 895825e..7199390 100644 --- a/bitorch/layers/extensions/layer_container.py +++ b/bitorch/layers/extensions/layer_container.py @@ -1,9 +1,21 @@ from typing import Any, TypeVar, Type, Generic +from bitorch.layers.extensions.layer_recipe import LayerRecipe + T = TypeVar("T") class LayerContainer(Generic[T]): + + internal_variable_names = [ + "_layer_implementation", + "_recipe", + ] + + patch = [ + "to", + ] + """ This class wraps another layer - but the internally contained class can be swapped out during runtime. """ @@ -16,6 +28,7 @@ def __init__(self, impl_class: Type[T], *args: Any, **kwargs: Any) -> None: **kwargs: keyword arguments of the new object """ self._layer_implementation = impl_class(*args, **kwargs) + self._recipe = LayerRecipe(layer=self, args=args, kwargs=kwargs) def replace_layer_implementation(self, new_implementation: T) -> None: """ @@ -26,13 +39,13 @@ def replace_layer_implementation(self, new_implementation: T) -> None: self._layer_implementation = new_implementation def __getattr__(self, item: Any) -> Any: - if item == "_layer_implementation": + if item in self.internal_variable_names: return self.__dict__[item] attr_value = getattr(self._layer_implementation, item) if attr_value == self._layer_implementation: return self - if callable(attr_value): - # dirty patch functions and classes + if callable(attr_value) and item in self.patch: + # patch return values of all functions/classes defined in self.patch # they should return this LayerContainer instead of themselves # required for e.g. pytorch's .to(device) function other = self @@ -62,7 +75,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._layer_implementation(*args, **kwargs) # type:ignore[operator] def __setattr__(self, key: Any, value: Any) -> None: - if key == "_layer_implementation": + if key in self.internal_variable_names: self.__dict__[key] = value return setattr(self._layer_implementation, key, value) @@ -79,3 +92,7 @@ def layer_implementation(self) -> T: the internal layer object """ return self._layer_implementation + + @property + def recipe(self) -> LayerRecipe: + return self._recipe diff --git a/bitorch/layers/extensions/layer_recipe.py b/bitorch/layers/extensions/layer_recipe.py index f660885..ed854a2 100644 --- a/bitorch/layers/extensions/layer_recipe.py +++ b/bitorch/layers/extensions/layer_recipe.py @@ -1,7 +1,9 @@ +import typing from dataclasses import dataclass from typing import TypeVar, Tuple, Any, Dict -from .layer_container import LayerContainer +if typing.TYPE_CHECKING: + from .layer_container import LayerContainer T = TypeVar("T") diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index 2afdcdd..a8c17b0 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -1,10 +1,10 @@ from abc import ABC from typing import Any, Type, Union, Tuple, TYPE_CHECKING -import bitorch import torch -from bitorch import runtime_mode_type, RuntimeMode +import bitorch +from bitorch import runtime_mode_type, RuntimeMode from .layer_container import LayerContainer from .layer_implementation import DefaultImplementationMixin, BaseImplementation, CustomImplementationMixin from .layer_recipe import LayerRecipe @@ -76,7 +76,7 @@ def _provide_layer_implementation(self, *args: Any, **kwargs: Any) -> LayerConta if self == correct_layer_implementation: # this class provides the correct implementation for the current mode (recursion stop) layer_container = LayerContainer(self.class_, *args, **kwargs) - self.registry.add_recipe(LayerRecipe(layer=layer_container, args=args, kwargs=kwargs)) + self.registry.add_recipe(layer_container.recipe) return layer_container # call this method again but on the correct base class return correct_layer_implementation._provide_layer_implementation(*args, **kwargs) diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 31e5770..b45dd97 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -1,6 +1,6 @@ """Module containing the quantized 2d convolution layer""" -from typing import Union, Any +from typing import Union, Any, Dict from torch import Tensor from torch.nn import Conv2d, init @@ -9,7 +9,7 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import DefaultImplementationMixin +from .extensions import DefaultImplementationMixin, LayerRecipe from .qactivation import QActivation from .register import QConv2dImplementation @@ -89,6 +89,38 @@ def __init__(self, # type: ignore super().__init__(*args, weight_quantization=weight_quantization, **kwargs) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) + @staticmethod + def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: + """ + Gather all arguments that were used to create a QLinear layer with argument names. + Can be used to recreate a layer with identical arguments. + + Returns: + A dictionary with all arguments (key is the argument name as a string even for positional arguments) + """ + _ = ... + return { + # "in_features": recipe.get_positional_arg(0), + # "out_features": recipe.get_positional_arg(1), + # "input_quantization": recipe.layer.input_quantization, + # "gradient_cancellation_threshold": recipe.layer.gradient_cancellation_threshold, + # "weight_quantization": recipe.layer.weight_quantization, + # "bias": recipe.get_arg(5, "bias", True), + "in_channels": _, + "out_channels": _, + "kernel_size": _, + "stride": _, + "padding": _, + "dilation": _, + "transposed": _, + "output_padding": _, + "groups": _, + "bias": _, + "padding_mode": _, + "device": recipe.get_arg(6, "device", None), + "dtype": recipe.get_arg(7, "dtype", None), + } + def forward(self, input_tensor: Tensor) -> Tensor: """forward the input tensor through the activation and quantized convolution layer. diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 6add873..1ba0f70 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -12,7 +12,7 @@ TEST_MODE = RuntimeMode.INFERENCE_AUTO -class TestLayerBase: +class LayerBase: def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val @@ -24,25 +24,22 @@ def class_name(self) -> str: return self.__class__.__name__ -test_registry = LayerRegistry("TestLayer") +test_registry = LayerRegistry("Layer") -class TestLayerImplementation(LayerImplementation): +class LayerImplementation(LayerImplementation): def __init__(self, *args): super().__init__(test_registry, *args) -@TestLayerImplementation(RuntimeMode.DEFAULT) -class TestLayerDefaultMode(DefaultImplementationMixin, TestLayerBase): - """Designate the TestLayerBase as the Default Mode""" +@LayerImplementation(RuntimeMode.DEFAULT) +class Layer(DefaultImplementationMixin, LayerBase): + """Designate the LayerBase as the Default Mode""" pass -TestLayer = TestLayerDefaultMode - - -@TestLayerImplementation(TEST_MODE) -class TestLayerCustomMode(CustomImplementationMixin, TestLayerBase): +@LayerImplementation(TEST_MODE) +class CustomLayerImplementation(CustomImplementationMixin, LayerBase): @classmethod def can_clone(cls, recipe: LayerRecipe) -> bool: # assume this test class can only clone layers with 'vals' lower than 100 @@ -70,8 +67,8 @@ def clean_environment(): def test_recipe(): - s1 = TestLayer("Hello World", val=21) - s2 = TestLayer("Hello World", 21) + s1 = Layer("Hello World", val=21) + s2 = Layer("Hello World", 21) s1_recipe = test_registry.get_recipe_for(s1) assert s1_recipe.args[0] == "Hello World" @@ -83,43 +80,43 @@ def test_recipe(): def test_default_impl(): - s = TestLayer("Hello World", val=21) + s = Layer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerDefaultMode" - assert isinstance(s, TestLayerDefaultMode.class_) + assert s.class_name() == "Layer" + assert isinstance(s, Layer.class_) assert isinstance(s, LayerContainer) def test_train_impl(): bitorch.mode = TEST_MODE - s = TestLayer("Hello World", val=21) + s = Layer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerCustomMode" - assert isinstance(s, TestLayerCustomMode) + assert s.class_name() == "CustomLayerImplementation" + assert isinstance(s, CustomLayerImplementation) assert isinstance(s, LayerContainer) def test_raw_impl(): bitorch.mode = RuntimeMode.RAW - s = TestLayer("Hello World", val=21) + s = Layer("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "TestLayerDefaultMode" - assert isinstance(s, TestLayer.class_) + assert s.class_name() == "Layer" + assert isinstance(s, Layer.class_) assert not isinstance(s, LayerContainer) @pytest.mark.parametrize("val, is_supported", [(150, False), (50, True)]) def test_clone(val, is_supported): - s = TestLayer("Hello World", val=val) + s = Layer("Hello World", val=val) s_recipe = test_registry.get_recipe_for(s) if is_supported: replacement = test_registry.get_replacement(TEST_MODE, s_recipe) - assert isinstance(replacement, TestLayerCustomMode) # type: ignore + assert isinstance(replacement, CustomLayerImplementation) # type: ignore else: with pytest.raises(RuntimeError) as e_info: _ = test_registry.get_replacement(TEST_MODE, s_recipe) error_message = str(e_info.value) assert e_info.typename == "RuntimeError" - expected_key_strings = ["TestLayer", "implementation", str(TEST_MODE), "val", "100"] + expected_key_strings = ["Layer", "implementation", str(TEST_MODE), "val", "100"] for key in expected_key_strings: assert key in error_message diff --git a/tests/layers/test_qconv.py b/tests/layers/test_qconv.py index 5596c2c..b523471 100644 --- a/tests/layers/test_qconv.py +++ b/tests/layers/test_qconv.py @@ -57,3 +57,22 @@ def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): assert torch.equal(padded_input, layer._apply_padding(expected_tensor)) assert torch.equal(result1, direct_result) assert torch.equal(grad1, grad2) + + +@pytest.mark.parametrize("all_args", [ + [ + ("in_channels", 16), + ("out_channels", 64), + ("kernel_size", 3), + ("stride", 1), + ("padding", 1), + ("dilation", 1), + ("groups", 1), + ("bias", True), + ("padding_mode", "zeros"), + ("device", None), + ("dtype", None), + ], +]) +def test_args_function(all_args): + pass diff --git a/tests/layers/test_qlinear.py b/tests/layers/test_qlinear.py index 158dd0b..ce307f6 100644 --- a/tests/layers/test_qlinear.py +++ b/tests/layers/test_qlinear.py @@ -1,10 +1,10 @@ import pytest -from bitorch.layers.qlinear import QLinear -from bitorch.layers.qactivation import QActivation -from bitorch.quantizations import Sign, quantization_from_name import torch from torch.nn import Parameter +from bitorch.layers.qactivation import QActivation +from bitorch.layers.qlinear import QLinear, QLinearBase +from bitorch.quantizations import Sign, quantization_from_name TEST_INPUT_DATA = [ [0., 0.], @@ -34,3 +34,36 @@ def test_qlinear(input_values, quantization): y = layer(x) assert torch.equal(result, y) + + +@pytest.mark.parametrize("all_args", [ + [ + ("in_features", 64), + ("out_features", 32), + ("input_quantization", Sign()), + ("gradient_cancellation_threshold", 1.3), + ("weight_quantization", Sign()), + ("bias", False), + ("device", None), + ("dtype", None), + ], +]) +def test_args_function(all_args): + positional_args = 2 + + expected_result = {} + layer_args = [] + layer_kwargs = {} + + for j, (key, val) in enumerate(all_args): + expected_result[key] = val + if j < positional_args: + layer_args.append(val) + else: + layer_kwargs[key] = val + + layer = QLinear(*layer_args, **layer_kwargs) + result = QLinearBase.get_args_as_kwargs(layer.recipe) + assert result.keys() == expected_result.keys() + for k in expected_result.keys(): + assert expected_result[k] == result[k] diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index ec5c72f..2d27275 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -26,10 +26,16 @@ def self_function(self): return self +class TestLayerContainer(LayerContainer): + patch = LayerContainer.patch + [ + "self_function", + ] + + @pytest.mark.parametrize("test_wrapped_layer", [False, True]) def test_switchable_layer(test_wrapped_layer): if test_wrapped_layer: - layer = LayerContainer(Layer, 42) + layer = TestLayerContainer(Layer, 42) else: layer = Layer(42) assert layer.x == 42 @@ -38,16 +44,18 @@ def test_switchable_layer(test_wrapped_layer): assert layer.self_function() == layer assert layer.self_property == layer - def test_class_assertions(layer_): - assert isinstance(layer_, nn.Module) - assert isinstance(layer_, Layer) - assert isinstance(layer_.foo, Foo) - assert isinstance(layer_.get_foo(), Foo) - assert test_wrapped_layer == isinstance(layer_, LayerContainer) - - test_class_assertions(layer) + assert isinstance(layer, nn.Module) + assert isinstance(layer, Layer) + assert isinstance(layer.foo, Foo) + assert isinstance(layer.get_foo(), Foo) + assert test_wrapped_layer == isinstance(layer, LayerContainer) moved_layer = layer.to(torch.device("cpu")) - test_class_assertions(moved_layer) + assert isinstance(layer, nn.Module) + assert isinstance(layer, Layer) + assert isinstance(layer.foo, Foo) + assert isinstance(layer.get_foo(), Foo) + assert test_wrapped_layer == isinstance(layer, LayerContainer) + assert layer == moved_layer From d50a438ec04c9e0d9e2862e5c1bfc3ca8c7b35d5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 22 Jun 2022 11:45:05 +0200 Subject: [PATCH 043/208] removed superfluous .to --- bitorch/layers/__init__.py | 1 - bitorch/layers/extensions/layer_registry.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 21b82ff..f910641 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -53,5 +53,4 @@ def convert(module: T, new_mode: RuntimeMode, device: torch.device = None, verbo submodules = list(module.modules()) for registry in all_layer_registries(): registry.convert_layers_to(new_mode, only=submodules, device=device, verbose=verbose) - module.to(device) return module diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index 942dece..a5207a0 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -106,7 +106,8 @@ def unregister_custom_implementations(self) -> None: self.layer_implementations.remove(i) def convert_layers_to( - self, new_mode: RuntimeMode, + self, + new_mode: RuntimeMode, only: Optional[Iterable[Any]] = None, device: torch.device = None, verbose: bool = False From 687616ce99cc6d582663c63a8d6c3fae60cd3b5e Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 19 May 2022 11:56:14 +0200 Subject: [PATCH 044/208] configure black --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 374b58c..0b03205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,7 @@ requires = [ "wheel" ] build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 +target-version = ['py37', 'py38', 'py39', 'py310'] From d6b83e95da691841b5896f3dc7c0a87bab395e47 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 19 May 2022 13:59:14 +0200 Subject: [PATCH 045/208] remove comma to avoid black formatting --- bitorch/layers/pact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitorch/layers/pact.py b/bitorch/layers/pact.py index 18a272d..aa9ee87 100644 --- a/bitorch/layers/pact.py +++ b/bitorch/layers/pact.py @@ -23,7 +23,7 @@ def backward(ctx, output_gradient: torch.Tensor) -> Tuple[torch.Tensor, torch.Te # Backward function, I borrowed code from # https://github.com/obilaniu/GradOverride/blob/master/functional.py # We get dL / dy_q as a gradient - x, alpha, = ctx.saved_tensors + x, alpha = ctx.saved_tensors # Weight gradient is only valid when [0, alpha] # Actual gradient for alpha, # By applying Chain Rule, we get dL / dy_q * dy_q / dy * dy / dalpha @@ -44,7 +44,7 @@ class Pact(Module): def __init__(self, bits: int = None) -> None: super().__init__() - self.alpha = torch.nn.parameter.Parameter(torch.tensor(10.)) + self.alpha = torch.nn.parameter.Parameter(torch.tensor(10.0)) self.bits = bits or config.pact_bits def forward(self, x: torch.Tensor) -> torch.Tensor: From 6bd63a39ec5373f6cdf1ef43be579884380d9377 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 20 May 2022 10:34:56 +0200 Subject: [PATCH 046/208] add black dependency --- requirements-dev.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c620948..45bf004 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,11 +1,12 @@ +black build flake8 mypy~=0.920 +myst-nb +nbclient==0.5.13 +nbsphinx-link==1.3.0 +nbsphinx==0.8.8 pep8-naming pytest sphinx twine -myst-nb -nbclient==0.5.13 -nbsphinx==0.8.8 -nbsphinx-link==1.3.0 From 3791ed55c24ab28c44926d791f80b530cd653956 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 16 Jun 2022 17:35:21 +0200 Subject: [PATCH 047/208] add black check to ci --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ab3ff4..610a4cf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,8 +12,10 @@ before_script: script: - flake8 --version - mypy --version + - black --version - flake8 - mypy --config-file mypy.ini + - black . --check --verbose --diff --color codestyle: extends: .codestyle From 5589d56c2d3b1402f02ba9a11cb0130d85751b16 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 16 Jun 2022 18:37:44 +0200 Subject: [PATCH 048/208] move mypy type ignore comment --- examples/pytorch_lightning/image_classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index f57ef24..851237a 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -76,7 +76,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: log_model=True, name=args.wandb_experiment, save_dir=str(output_dir) - ) + ) # type: ignore ) callbacks = [] if args.checkpoint_dir is not None: From b038ec259bc0da19ae1df6d5a37deb4253200732 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 16 Jun 2022 18:38:19 +0200 Subject: [PATCH 049/208] add black code formatting pre-commit hock --- .pre-commit-config.yaml | 6 ++++++ requirements-dev.txt | 1 + 2 files changed, 7 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a9abd52 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3.8 \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 45bf004..d96fd6c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,3 +10,4 @@ pep8-naming pytest sphinx twine +pre-commit \ No newline at end of file From c8a2e28de5680ba063527f7917a6e57384e0c643 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Fri, 17 Jun 2022 19:16:15 +0200 Subject: [PATCH 050/208] add changelog entry and readme notes for black --- CHANGELOG.md | 4 ++++ README.md | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 313f264..6147b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Support for integration of bitorch's inference engine for the following layers - QLinear +### Added + +- code formatting using black + ### Changed - using PyTorch's implementation of RAdam diff --git a/README.md b/README.md index 6d65ad2..f954fd0 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,17 @@ The codebase has type annotations, please make sure to add type hints if require mypy --config-file mypy.ini ``` + +For code formatting we use `black`: +```bash +black . --check --verbose --diff --color +``` + +In order to automatically apply the code formatting with every commit, you can install pre-commit and use the pre-commit hook: +```bash +pre-commit install +``` + Finally, the tests can be run with: ```bash From dc87fa16a9a6ae0f400997d5581245cc9dd50234 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 22 Jun 2022 16:54:45 +0200 Subject: [PATCH 051/208] add comma to arg parser so all options are spanning over different lines after formatting --- README.md | 3 +- .../pytorch_lightning/utils/arg_parser.py | 60 +++++++++---------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f954fd0..22b9010 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ mypy --config-file mypy.ini For code formatting we use `black`: ```bash -black . --check --verbose --diff --color +black . --check --verbose --diff --color # check what changes the formatter would do +black . # apply the formatter ``` In order to automatically apply the code formatting with every commit, you can install pre-commit and use the pre-commit hook: diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index cd223fa..27ce8ef 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -17,38 +17,38 @@ def add_logging_args(parser: ArgumentParser) -> None: log = parser.add_argument_group("Logging", "parameters for logging") log.add_argument("--log-level", type=str, default="info", choices=["debug", "info", "warning", "error", "critical"], - help="log level for logging message output") + help="log level for logging message output",) log.add_argument("--log-interval", type=int, default=100, metavar="N", - help="how many batches to wait before logging training status") + help="how many batches to wait before logging training status",) log.add_argument("--log-file", type=str, default=None, - help="output file path for logging. default to stdout") + help="output file path for logging. default to stdout",) log.add_argument("--log-stdout", action="store_true", - help="toggles force logging to stdout. if a log file is specified, logging will be " - "printed to both the log file and stdout") + help="toggles force logging to stdout. if a log file is specified, logging will be" + "printed to both the log file and stdout",) log.add_argument("--result-directory", type=str, default="./logs", - help="path to logs directory, e.g. tensorboard logs, csv files") + help="path to logs directory, e.g. tensorboard logs, csv files",) log.add_argument("--disable-tensorboard-log", action="store_false", dest="tensorboard_log", - help="disables tensorboard logging") + help="disables tensorboard logging",) log.add_argument("--disable-csv-log", action="store_false", dest="csv_log", - help="disables csv logging") + help="disables csv logging",) log.add_argument("--wandb", action="store_true", dest="wandb_log", - help="enables wandb logging (WANDB_API_KEY environment variable must be set)") + help="enables wandb logging (WANDB_API_KEY environment variable must be set)",) log.add_argument("--wandb-project", type=str, default="bitorch", - help="name of wand project to be used by wandb logger") + help="name of wand project to be used by wandb logger",) log.add_argument("--wandb-experiment", type=str, default=None, - help="name of wand experiment to be used by wandb logger") + help="name of wand experiment to be used by wandb logger",) def add_checkpoint_args(parser: ArgumentParser) -> None: checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") checkpoint.add_argument("--checkpoint-dir", type=str, default=None, - help="set a custom path to store checkpoints in.") + help="set a custom path to store checkpoints in.",) checkpoint.add_argument("--checkpoint-keep-count", type=int, default=1, - help="number of checkpoints to keep.") + help="number of checkpoints to keep.",) checkpoint.add_argument("--checkpoint-load", type=str, default=None, - help="path to checkpoint file to load state from. if omitted, a new model will be trained.") + help="path to checkpoint file to load state from. if omitted, a new model will be trained.",) checkpoint.add_argument("--pretrained", action="store_true", - help="uses the given checkpoint as a pretrained model (only for initialization)") + help="uses the given checkpoint as a pretrained model (only for initialization)",) def add_optimizer_args(parser: ArgumentParser) -> None: @@ -60,17 +60,17 @@ def add_optimizer_args(parser: ArgumentParser) -> None: optimizer = parser.add_argument_group("Optimizer", "parameters for optimizer") optimizer.add_argument("--lr-scheduler", type=str, choices=["cosine", "step", "exponential"], - help="name of the lr scheduler to use. default to none") + help="name of the lr scheduler to use. default to none",) optimizer.add_argument("--lr", type=float, default=0.01, - help="initial learning rate (default: 0.01)") + help="initial learning rate (default: 0.01)",) optimizer.add_argument('--lr-factor', default=0.1, type=float, - help='learning rate decay ratio. this is used only by the step and exponential lr scheduler') + help='learning rate decay ratio. this is used only by the step and exponential lr scheduler',) optimizer.add_argument('--lr-steps', nargs="*", default=[30, 60, 90], - help='list of learning rate decay epochs as list. this is used only by the step scheduler') + help='list of learning rate decay epochs as list. this is used only by the step scheduler',) optimizer.add_argument('--momentum', type=float, default=0.9, - help='momentum value for optimizer, default is 0.9. only used for sgd optimizer') + help='momentum value for optimizer, default is 0.9. only used for sgd optimizer',) optimizer.add_argument('--optimizer', type=str, default="adam", choices=["adam", "sgd", "radam"], - help='the optimizer to use. default is adam.') + help='the optimizer to use. default is adam.',) def add_dataset_args(parser: ArgumentParser) -> None: @@ -81,20 +81,20 @@ def add_dataset_args(parser: ArgumentParser) -> None: """ data = parser.add_argument_group("dataset", "parameters for the dataset used for training") data.add_argument("--dataset", type=str, default="cifar10", choices=dataset_names(), - help="name of the dataset to be used for training") + help="name of the dataset to be used for training",) data.add_argument("--dataset-dir", type=str, default=None, - help="path to where the train dataset is saved / shall be downloaded to") + help="path to where the train dataset is saved / shall be downloaded to",) data.add_argument("--download", action="store_true", help="toggles wether the dataset shall be downloaded if not present. " - "only has effect with the cifar10 and mnist dataset so far.") + "only has effect with the cifar10 and mnist dataset so far.",) data.add_argument("--batch-size", type=int, default=128, - help="batch size for training and testing (default: 128)") + help="batch size for training and testing (default: 128)",) data.add_argument("--num-workers", type=int, default=4, - help="number of workers to be used for dataloading (default: 4)") + help="number of workers to be used for dataloading (default: 4)",) data.add_argument("--augmentation", type=str, choices=["none", "low", "medium", "high"], default="none", - help="level of augmentation to be used in data preparation (default 'none')") + help="level of augmentation to be used in data preparation (default 'none')",) data.add_argument("--fake-data", action="store_true", - help="train with fake data") + help="train with fake data",) def create_model_argparser(model_class: object) -> ArgumentParser: @@ -149,9 +149,9 @@ def add_regular_args(parser: ArgumentParser) -> None: add_config_args(parser) parser.add_argument("--model", type=str.lower, choices=model_names(), required=True, - help="name of the model to be trained") + help="name of the model to be trained",) parser.add_argument("--cpu", action="store_true", - help="explicitly use the cpu. overwrites gpu settings") + help="explicitly use the cpu. overwrites gpu settings",) def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: From 32fd0e5a128ba52643db32aa2d38cec77ffea257 Mon Sep 17 00:00:00 2001 From: Black Code Formatter <> Date: Wed, 22 Jun 2022 17:50:37 +0200 Subject: [PATCH 052/208] apply black formatting --- bitorch/config.py | 24 +- bitorch/datasets/__init__.py | 9 +- bitorch/datasets/base.py | 20 +- bitorch/datasets/cifar.py | 30 ++- bitorch/datasets/imagenet.py | 28 +- bitorch/datasets/mnist.py | 2 +- bitorch/layers/__init__.py | 31 ++- bitorch/layers/debug_layers.py | 30 +-- bitorch/layers/extensions/layer_container.py | 7 +- .../layers/extensions/layer_implementation.py | 3 + bitorch/layers/extensions/layer_recipe.py | 1 + .../layers/extensions/layer_registration.py | 14 +- bitorch/layers/extensions/layer_registry.py | 13 +- bitorch/layers/qactivation.py | 29 +-- bitorch/layers/qconv1d.py | 36 +-- bitorch/layers/qconv2d.py | 35 +-- bitorch/layers/qconv3d.py | 35 +-- bitorch/layers/qembedding.py | 89 ++++--- bitorch/layers/qlinear.py | 14 +- bitorch/layers/register.py | 9 +- bitorch/models/__init__.py | 17 +- bitorch/models/base.py | 5 +- bitorch/models/lenet.py | 26 +- bitorch/models/resnet.py | 109 ++++---- bitorch/models/resnet_e.py | 49 ++-- bitorch/quantizations/__init__.py | 10 +- bitorch/quantizations/approx_sign.py | 12 +- bitorch/quantizations/base.py | 9 +- bitorch/quantizations/dorefa.py | 8 +- bitorch/quantizations/sign.py | 8 +- bitorch/quantizations/ste_heaviside.py | 12 +- bitorch/quantizations/swish_sign.py | 18 +- bitorch/runtime_mode.py | 17 +- bitorch/util.py | 13 +- docs/source/conf.py | 37 +-- examples/mnist/train_mnist.py | 2 +- .../pytorch_lightning/image_classification.py | 62 +++-- .../pytorch_lightning/utils/arg_parser.py | 239 +++++++++++++----- .../utils/lightning_model.py | 48 ++-- examples/pytorch_lightning/utils/log.py | 13 +- examples/pytorch_lightning/utils/utils.py | 16 +- setup.py | 28 +- tests/layers/test_layer_impl.py | 3 +- tests/layers/test_pact.py | 2 +- tests/layers/test_qactivation.py | 2 +- tests/layers/test_qconv.py | 107 +++++--- tests/layers/test_qconv_noact.py | 6 +- tests/layers/test_qembeddings.py | 128 +++++++--- tests/layers/test_qlinear.py | 31 +-- tests/models/test_model_conversion.py | 12 +- tests/models/test_models.py | 9 +- tests/quantizations/test_quantizations.py | 85 +++++-- 52 files changed, 1014 insertions(+), 588 deletions(-) diff --git a/bitorch/config.py b/bitorch/config.py index f55c857..6605164 100644 --- a/bitorch/config.py +++ b/bitorch/config.py @@ -11,8 +11,10 @@ class Config: def __init__(self) -> None: """collects all attributes of class that are not the name as configurable attributes.""" configurable_attributes = [ - attribute for attribute in dir(self) - if not attribute.startswith('__') and not callable(getattr(self, attribute)) and not attribute == "name"] + attribute + for attribute in dir(self) + if not attribute.startswith("__") and not callable(getattr(self, attribute)) and not attribute == "name" + ] self._configurable_attributes = configurable_attributes for attribute in self._configurable_attributes: @@ -39,11 +41,21 @@ def add_config_arguments(self, parser: ArgumentParser) -> None: for attribute in self._configurable_attributes: attribute_value = getattr(self, attribute) if isinstance(attribute_value, bool): - config.add_argument(f"--{attribute.replace('_', '-')}", dest=attribute, default=attribute_value, - action=f"store_{'false' if attribute_value else 'true'}", required=False) + config.add_argument( + f"--{attribute.replace('_', '-')}", + dest=attribute, + default=attribute_value, + action=f"store_{'false' if attribute_value else 'true'}", + required=False, + ) else: - config.add_argument(f"--{attribute.replace('_', '-')}", dest=attribute, default=attribute_value, - type=type(attribute_value), required=False) + config.add_argument( + f"--{attribute.replace('_', '-')}", + dest=attribute, + default=attribute_value, + type=type(attribute_value), + required=False, + ) def apply_args_to_configuration(self, args: Namespace) -> None: """loads the cli set values of configurable attributes. diff --git a/bitorch/datasets/__init__.py b/bitorch/datasets/__init__.py index b870bd7..67bc853 100644 --- a/bitorch/datasets/__init__.py +++ b/bitorch/datasets/__init__.py @@ -12,8 +12,13 @@ from ..util import build_lookup_dictionary __all__ = [ - 'BasicDataset', 'dataset_from_name', 'dataset_names', - 'MNIST', 'CIFAR10', 'CIFAR100', 'ImageNet', + "BasicDataset", + "dataset_from_name", + "dataset_names", + "MNIST", + "CIFAR10", + "CIFAR100", + "ImageNet", ] diff --git a/bitorch/datasets/base.py b/bitorch/datasets/base.py index 35ee0fa..9e144d4 100644 --- a/bitorch/datasets/base.py +++ b/bitorch/datasets/base.py @@ -39,11 +39,12 @@ class BasicDataset(Dataset): num_val_samples = 0 def __init__( - self, - train: bool, - root_directory: str = None, - download: bool = False, - augmentation: Augmentation = Augmentation.DEFAULT) -> None: + self, + train: bool, + root_directory: str = None, + download: bool = False, + augmentation: Augmentation = Augmentation.DEFAULT, + ) -> None: """initializes the dataset. Args: @@ -64,10 +65,11 @@ def __init__( @classmethod def get_train_and_test( - cls, - root_directory: Optional[str] = None, - download: bool = False, - augmentation: Augmentation = Augmentation.DEFAULT) -> Tuple["BasicDataset", "BasicDataset"]: + cls, + root_directory: Optional[str] = None, + download: bool = False, + augmentation: Augmentation = Augmentation.DEFAULT, + ) -> Tuple["BasicDataset", "BasicDataset"]: """creates a pair of train and test dataset. Returns: diff --git a/bitorch/datasets/cifar.py b/bitorch/datasets/cifar.py index 7084f6b..4e4264a 100644 --- a/bitorch/datasets/cifar.py +++ b/bitorch/datasets/cifar.py @@ -6,7 +6,7 @@ from .base import BasicDataset, Augmentation -__all__ = ['CIFAR10', 'CIFAR100'] +__all__ = ["CIFAR10", "CIFAR100"] class CIFAR(BasicDataset, ABC): @@ -16,19 +16,23 @@ class CIFAR(BasicDataset, ABC): @classmethod def train_transform(cls, augmentation: Augmentation = Augmentation.DEFAULT) -> transforms.Compose: - return transforms.Compose([ - transforms.RandomCrop(32, padding=4), - transforms.RandomHorizontalFlip(), - transforms.ToTensor(), - cls.get_normalize_transform(), - ]) + return transforms.Compose( + [ + transforms.RandomCrop(32, padding=4), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) @classmethod def test_transform(cls) -> transforms.Compose: - return transforms.Compose([ - transforms.ToTensor(), - cls.get_normalize_transform(), - ]) + return transforms.Compose( + [ + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) class CIFAR10(CIFAR): @@ -42,7 +46,7 @@ def get_dataset(self, download: bool = True) -> Dataset: root=self.root_directory, train=self.is_train, transform=self.get_transform(), - download=download + download=download, ) @@ -57,5 +61,5 @@ def get_dataset(self, download: bool = True) -> Dataset: root=self.root_directory, train=self.is_train, transform=self.get_transform(), - download=download + download=download, ) diff --git a/bitorch/datasets/imagenet.py b/bitorch/datasets/imagenet.py index b68355e..f399a57 100644 --- a/bitorch/datasets/imagenet.py +++ b/bitorch/datasets/imagenet.py @@ -32,18 +32,22 @@ def get_dataset(self, download: bool) -> Dataset: @classmethod def train_transform(cls, augmentation: Augmentation = Augmentation.DEFAULT) -> transforms.Compose: crop_scale = 0.08 - return transforms.Compose([ - transforms.RandomResizedCrop(224, scale=(crop_scale, 1.0)), - transforms.RandomHorizontalFlip(), - transforms.ToTensor(), - cls.get_normalize_transform(), - ]) + return transforms.Compose( + [ + transforms.RandomResizedCrop(224, scale=(crop_scale, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) @classmethod def test_transform(cls) -> transforms.Compose: - return transforms.Compose([ - transforms.Resize(256), - transforms.CenterCrop(224), - transforms.ToTensor(), - cls.get_normalize_transform(), - ]) + return transforms.Compose( + [ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) diff --git a/bitorch/datasets/mnist.py b/bitorch/datasets/mnist.py index 5121f04..fde7f8e 100644 --- a/bitorch/datasets/mnist.py +++ b/bitorch/datasets/mnist.py @@ -19,5 +19,5 @@ def get_dataset(self, download: bool = True) -> Dataset: root=self.root_directory, train=self.is_train, transform=self.get_transform(), - download=download + download=download, ) diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index f910641..7e9948f 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -9,13 +9,7 @@ from torch import nn from bitorch import RuntimeMode -from .debug_layers import ( - InputGraphicalDebug, - InputPrintDebug, - WeightGraphicalDebug, - WeightPrintDebug, - ShapePrintDebug -) +from .debug_layers import InputGraphicalDebug, InputPrintDebug, WeightGraphicalDebug, WeightPrintDebug, ShapePrintDebug from .extensions import CustomImplementationMixin, LayerRegistry from .pact import Pact from .qactivation import QActivation @@ -27,10 +21,25 @@ from .register import all_layer_registries __all__ = [ - "InputGraphicalDebug", "InputPrintDebug", "WeightGraphicalDebug", "WeightPrintDebug", - "ShapePrintDebug", "QActivation", "QConv1d", "QConv2d", "QConv3d", "QConv1d_NoAct", - "QConv2d_NoAct", "QConv3d_NoAct", "QLinear", "QLinearBase", "QEmbedding", "QEmbeddingBag", "Pact", - "CustomImplementationMixin", "convert" + "InputGraphicalDebug", + "InputPrintDebug", + "WeightGraphicalDebug", + "WeightPrintDebug", + "ShapePrintDebug", + "QActivation", + "QConv1d", + "QConv2d", + "QConv3d", + "QConv1d_NoAct", + "QConv2d_NoAct", + "QConv3d_NoAct", + "QLinear", + "QLinearBase", + "QEmbedding", + "QEmbeddingBag", + "Pact", + "CustomImplementationMixin", + "convert", ] diff --git a/bitorch/layers/debug_layers.py b/bitorch/layers/debug_layers.py index 617e86b..a3bc19b 100644 --- a/bitorch/layers/debug_layers.py +++ b/bitorch/layers/debug_layers.py @@ -4,10 +4,7 @@ class _Debug(torch.nn.Module): - def __init__(self, - debug_interval: int = 100, - num_outputs: int = 10, - name: str = "Debug") -> None: + def __init__(self, debug_interval: int = 100, num_outputs: int = 10, name: str = "Debug") -> None: """inits values. Args: @@ -44,17 +41,19 @@ def _debug(self, debug_tensor: torch.Tensor) -> None: Args: debug_tensor (torch.Tensor): tensor to be debugged """ - print(self.name, ":", debug_tensor if len(debug_tensor) < - self._num_outputs else debug_tensor[:self._num_outputs]) + print( + self.name, ":", debug_tensor if len(debug_tensor) < self._num_outputs else debug_tensor[: self._num_outputs] + ) class _GraphicalDebug(_Debug): - - def __init__(self, - figure: object = None, - images: list = None, - debug_interval: int = 100, - num_outputs: int = 10) -> None: + def __init__( + self, + figure: object = None, + images: list = None, + debug_interval: int = 100, + num_outputs: int = 10, + ) -> None: """Debugs the given layer by drawing weights/inputs in given matplotlib plot images. Args: @@ -91,7 +90,8 @@ def set_images(self, images: list = None) -> None: if self._images is not None and len(self._images) != self._num_outputs: raise ValueError( f"number of given images ({len(self._images)}) must match " - f"number of desired outputs ({self._num_outputs})!") + f"number of desired outputs ({self._num_outputs})!" + ) def _debug(self, debug_tensor: torch.Tensor) -> None: """draws graphical debug information about given debug tensor into figure @@ -183,7 +183,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: weight = self._debug_module.weight.clone() # type: ignore # check if given module is a quantized module - if hasattr(self._debug_module, 'quantize'): + if hasattr(self._debug_module, "quantize"): weight = self._debug_module.quantize(weight) # type: ignore self._debug_tensor(weight) @@ -213,7 +213,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: weight = self._debug_module.weight.clone() # type: ignore # check if given module is a quantized module - if hasattr(self._debug_module, 'quantize'): + if hasattr(self._debug_module, "quantize"): weight = self._debug_module.quantize(weight) # type: ignore self._debug_tensor(weight) diff --git a/bitorch/layers/extensions/layer_container.py b/bitorch/layers/extensions/layer_container.py index 7199390..c2c4979 100644 --- a/bitorch/layers/extensions/layer_container.py +++ b/bitorch/layers/extensions/layer_container.py @@ -7,6 +7,10 @@ class LayerContainer(Generic[T]): + """ + This class wraps another layer - but the internally contained class can be swapped out during runtime. + """ + internal_variable_names = [ "_layer_implementation", "_recipe", @@ -16,9 +20,6 @@ class LayerContainer(Generic[T]): "to", ] - """ - This class wraps another layer - but the internally contained class can be swapped out during runtime. - """ def __init__(self, impl_class: Type[T], *args: Any, **kwargs: Any) -> None: """ Wrap a new object based on the given class, positional arguments, and keyword arguments. diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index a27cae8..222102d 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -9,6 +9,7 @@ class BaseImplementation: """Defines the class interface of a custom layer implementation of a certain layer type.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -50,6 +51,7 @@ def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) - class DefaultImplementationMixin(BaseImplementation, ABC): """Defines the class interface of a default layer implementation of a certain layer type.""" + @classmethod def is_default_implementation(cls) -> bool: return True @@ -65,6 +67,7 @@ def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) - class CustomImplementationMixin(BaseImplementation, ABC): """Defines the class interface of a custom layer implementation of a certain layer type.""" + @classmethod def is_default_implementation(cls) -> bool: return False diff --git a/bitorch/layers/extensions/layer_recipe.py b/bitorch/layers/extensions/layer_recipe.py index ed854a2..17197d9 100644 --- a/bitorch/layers/extensions/layer_recipe.py +++ b/bitorch/layers/extensions/layer_recipe.py @@ -14,6 +14,7 @@ class LayerRecipe: Data class to store a layer object and the arguments used to create it. It allows to create other implementations of the same layer later on. """ + layer: "LayerContainer" args: Tuple[Any, ...] kwargs: Dict[str, Any] diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index a8c17b0..fd2e0bc 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -43,7 +43,9 @@ def __init__(self, registry: "LayerRegistry", supported_modes: runtime_mode_type self.class_ = None # type: ignore self.class_name = "" - def __call__(self, *args: Any, **kwargs: Any) -> Union["LayerImplementation", Type[BaseImplementation], LayerContainer]: + def __call__( + self, *args: Any, **kwargs: Any + ) -> Union["LayerImplementation", Type[BaseImplementation], LayerContainer]: if not self.__initialized: # this object is called once when @Decorator is used, we need to initialize return self._initialize(*args, **kwargs) @@ -60,14 +62,16 @@ def _initialize(self, class_: Type[BaseImplementation]) -> Union["LayerImplement self.class_name = self.class_.__name__ self.registry.register(self) if self._supported_modes == RuntimeMode.DEFAULT: - assert issubclass(self.class_, DefaultImplementationMixin), \ - f"{self.class_name} should be a subclass of DefaultLayerImplementation." + assert issubclass( + self.class_, DefaultImplementationMixin + ), f"{self.class_name} should be a subclass of DefaultLayerImplementation." # provide this wrapper return self else: - assert issubclass(self.class_, CustomImplementationMixin), \ - f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " \ + assert issubclass(self.class_, CustomImplementationMixin), ( + f"{self.class_name} should be a subclass of CustomImplementationInterface (and it should " f"implement the corresponding class methods)." + ) # after we have registered custom implementations, we do not interfere anymore return self.class_ diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index a5207a0..0f7820d 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -15,6 +15,7 @@ class LayerRegistry: It also wraps these implementations and stores references to them, so they can be replaced easily. Needs to be subclassed for each type of layer. """ + def __init__(self, name: str) -> None: self.name = name self._class = None @@ -53,7 +54,7 @@ def register(self, layer: LayerImplementation) -> None: self.layer_implementations.add(layer) def get_layer( - self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None + self, mode: Optional[RuntimeMode] = None, recipe: Optional[LayerRecipe] = None ) -> LayerImplementation: """ Get a layer implementation compatible to the given mode and recipe. @@ -106,11 +107,11 @@ def unregister_custom_implementations(self) -> None: self.layer_implementations.remove(i) def convert_layers_to( - self, - new_mode: RuntimeMode, - only: Optional[Iterable[Any]] = None, - device: torch.device = None, - verbose: bool = False + self, + new_mode: RuntimeMode, + only: Optional[Iterable[Any]] = None, + device: torch.device = None, + verbose: bool = False, ) -> None: for recipe in list(self._instance_recipes): module = recipe.layer diff --git a/bitorch/layers/qactivation.py b/bitorch/layers/qactivation.py index 4833c1b..d21f8f6 100644 --- a/bitorch/layers/qactivation.py +++ b/bitorch/layers/qactivation.py @@ -9,13 +9,13 @@ class GradientCancellation(Function): - @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor, - threshold: float) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + threshold: float, + ) -> torch.Tensor: """Binarize input tensor using the _sign function. Args: @@ -30,8 +30,9 @@ def forward( @staticmethod @typing.no_type_check def backward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - output_grad: torch.Tensor) -> Tuple[torch.Tensor, None]: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + output_grad: torch.Tensor, + ) -> Tuple[torch.Tensor, None]: """Apply straight through estimator. This passes the output gradient towards the input if the inputs are in the range [-1, 1]. @@ -45,9 +46,8 @@ def backward( """ input_tensor, threshold = ctx.saved_tensors cancelled = torch.where( - torch.abs(input_tensor) <= threshold, - output_grad, - torch.tensor(0., device=output_grad.device)) + torch.abs(input_tensor) <= threshold, output_grad, torch.tensor(0.0, device=output_grad.device) + ) return cancelled, None @@ -55,9 +55,10 @@ class QActivation(nn.Module): """Activation layer for quantization""" def __init__( - self, - activation: Union[str, Quantization] = None, - gradient_cancellation_threshold: Optional[float] = 0.0) -> None: + self, + activation: Union[str, Quantization] = None, + gradient_cancellation_threshold: Optional[float] = 0.0, + ) -> None: """initialization function for fetching suitable activation function. Args: @@ -68,9 +69,7 @@ def __init__( """ super(QActivation, self).__init__() self.activation_function = config.get_quantization_function(activation or config.input_quantization) - self.gradient_cancellation_threshold = ( - gradient_cancellation_threshold or config.gradient_cancellation_threshold - ) + self.gradient_cancellation_threshold = gradient_cancellation_threshold or config.gradient_cancellation_threshold def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: """Forwards input tensor through activation function. diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index ae9febe..3566e03 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -17,12 +17,15 @@ class QConv1d_NoAct(Conv1d): # noqa: N801 """Quantized 1d Convolutional Layer. Has the same api as Conv1d but lets you specify a weight quantization, that is applied before the convolutional operation.""" - def __init__(self, - *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, - bias: bool = False, - **kwargs: Any) -> None: + + def __init__( + self, + *args: Any, + weight_quantization: Union[str, Quantization] = None, + pad_value: float = None, + bias: bool = False, + **kwargs: Any, + ) -> None: """initialization function for padding and quantization. Args: @@ -33,8 +36,7 @@ def __init__(self, assert bias is False, "A QConv layer can not use a bias due to acceleration techniques during deployment." kwargs["bias"] = False super(QConv1d_NoAct, self).__init__(*args, **kwargs) - self._weight_quantize = config.get_quantization_function( - weight_quantization or config.weight_quantization) + self._weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) self._pad_value = pad_value or config.padding_value def _apply_padding(self, x: Tensor) -> Tensor: @@ -68,16 +70,19 @@ def forward(self, input: Tensor) -> Tensor: stride=self.stride, padding=0, dilation=self.dilation, - groups=self.groups) + groups=self.groups, + ) class QConv1dBase(QConv1d_NoAct): # type: ignore - def __init__(self, # type: ignore - *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, - gradient_cancellation_threshold: Union[float, None] = None, - **kwargs: Any) -> None: + def __init__( + self, # type: ignore + *args: Any, + input_quantization: Union[str, Quantization] = None, + weight_quantization: Union[str, Quantization] = None, + gradient_cancellation_threshold: Union[float, None] = None, + **kwargs: Any, + ) -> None: """initialization function for quantization of inputs and weights. Args: @@ -110,4 +115,5 @@ class QConv1d(DefaultImplementationMixin, QConv1dBase): To implement a custom QConv1d implementation use QConv1dBase as a super class instead. """ + pass diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index b45dd97..c8a2d8f 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -15,12 +15,14 @@ class QConv2d_NoAct(Conv2d): # type: ignore # noqa: N801 - def __init__(self, # type: ignore - *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, - bias: bool = False, - **kwargs: Any) -> None: + def __init__( + self, # type: ignore + *args: Any, + weight_quantization: Union[str, Quantization] = None, + pad_value: float = None, + bias: bool = False, + **kwargs: Any, + ) -> None: """initialization function for padding and quantization. Args: @@ -31,8 +33,7 @@ def __init__(self, # type: ignore assert bias is False, "A QConv layer can not use a bias due to acceleration techniques during deployment." kwargs["bias"] = False super(QConv2d_NoAct, self).__init__(*args, **kwargs) - self._weight_quantize = config.get_quantization_function( - weight_quantization or config.weight_quantization) + self._weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) self._pad_value = pad_value or config.padding_value def _apply_padding(self, x: Tensor) -> Tensor: @@ -66,16 +67,19 @@ def forward(self, input: Tensor) -> Tensor: stride=self.stride, padding=0, dilation=self.dilation, - groups=self.groups) + groups=self.groups, + ) class QConv2dBase(QConv2d_NoAct): # type: ignore - def __init__(self, # type: ignore - *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, - gradient_cancellation_threshold: Union[float, None] = None, - **kwargs: Any) -> None: + def __init__( + self, # type: ignore + *args: Any, + input_quantization: Union[str, Quantization] = None, + weight_quantization: Union[str, Quantization] = None, + gradient_cancellation_threshold: Union[float, None] = None, + **kwargs: Any, + ) -> None: """initialization function for quantization of inputs and weights. Args: @@ -140,4 +144,5 @@ class QConv2d(DefaultImplementationMixin, QConv2dBase): To implement a custom QConv2d implementation use QConv2dBase as a super class instead. """ + pass diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index aaa582a..665d0d7 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -15,12 +15,14 @@ class QConv3d_NoAct(Conv3d): # type: ignore # noqa: N801 - def __init__(self, # type: ignore - *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, - bias: bool = False, - **kwargs: Any) -> None: + def __init__( + self, # type: ignore + *args: Any, + weight_quantization: Union[str, Quantization] = None, + pad_value: float = None, + bias: bool = False, + **kwargs: Any, + ) -> None: """initialization function for padding and quantization. Args: @@ -31,8 +33,7 @@ def __init__(self, # type: ignore assert bias is False, "A QConv layer can not use a bias due to acceleration techniques during deployment." kwargs["bias"] = False super(QConv3d_NoAct, self).__init__(*args, **kwargs) - self._weight_quantize = config.get_quantization_function( - weight_quantization or config.weight_quantization) + self._weight_quantize = config.get_quantization_function(weight_quantization or config.weight_quantization) self._pad_value = pad_value or config.padding_value def _apply_padding(self, x: Tensor) -> Tensor: @@ -66,16 +67,19 @@ def forward(self, input: Tensor) -> Tensor: stride=self.stride, padding=0, dilation=self.dilation, - groups=self.groups) + groups=self.groups, + ) class QConv3dBase(QConv3d_NoAct): # type: ignore - def __init__(self, # type: ignore - *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, - gradient_cancellation_threshold: Union[float, None] = None, - **kwargs: Any) -> None: + def __init__( + self, # type: ignore + *args: Any, + input_quantization: Union[str, Quantization] = None, + weight_quantization: Union[str, Quantization] = None, + gradient_cancellation_threshold: Union[float, None] = None, + **kwargs: Any, + ) -> None: """initialization function for quantization of inputs and weights. Args: @@ -108,4 +112,5 @@ class QConv3d(DefaultImplementationMixin, QConv3dBase): To implement a custom QConv3d implementation use QConv3dBase as a super class instead. """ + pass diff --git a/bitorch/layers/qembedding.py b/bitorch/layers/qembedding.py index da6b95a..abaeee6 100644 --- a/bitorch/layers/qembedding.py +++ b/bitorch/layers/qembedding.py @@ -14,24 +14,28 @@ class QEmbeddingBag(EmbeddingBag): """ def __init__( - self, - *args: int, - embedding_dim: int, - weight_quantization: Union[Quantization, str] = None, - output_quantization: Union[Quantization, str] = None, - **kwargs: int) -> None: + self, + *args: int, + embedding_dim: int, + weight_quantization: Union[Quantization, str] = None, + output_quantization: Union[Quantization, str] = None, + **kwargs: int, + ) -> None: super(QEmbeddingBag, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore """load quantization functions""" self.embedding_weight_quantization = config.get_quantization_function( - weight_quantization or config.weight_quantization) + weight_quantization or config.weight_quantization + ) self.embedding_input_quantization = config.get_quantization_function( - output_quantization or config.input_quantization) + output_quantization or config.input_quantization + ) def forward( - self, - input: Tensor, - offsets: Optional[Tensor] = None, - per_sample_weights: Optional[Tensor] = None) -> Tensor: + self, + input: Tensor, + offsets: Optional[Tensor] = None, + per_sample_weights: Optional[Tensor] = None, + ) -> Tensor: """generates embeddings for received bags. then quantizes these embeddings and depending on configuration forwards it through another quantized linear layer. @@ -45,20 +49,32 @@ def forward( """ # necessary for torch 1.8 compliance - if hasattr(self, 'padding_idx'): + if hasattr(self, "padding_idx"): embeddings = embedding_bag( - input, self.embedding_weight_quantization(self.weight), offsets, - self.max_norm, self.norm_type, - self.scale_grad_by_freq, self.mode, self.sparse, - per_sample_weights, self.include_last_offset, - self.padding_idx + input, + self.embedding_weight_quantization(self.weight), + offsets, + self.max_norm, + self.norm_type, + self.scale_grad_by_freq, + self.mode, + self.sparse, + per_sample_weights, + self.include_last_offset, + self.padding_idx, ) else: embeddings = embedding_bag( - input, self.embedding_weight_quantization(self.weight), offsets, - self.max_norm, self.norm_type, - self.scale_grad_by_freq, self.mode, self.sparse, - per_sample_weights, self.include_last_offset, + input, + self.embedding_weight_quantization(self.weight), + offsets, + self.max_norm, + self.norm_type, + self.scale_grad_by_freq, + self.mode, + self.sparse, + per_sample_weights, + self.include_last_offset, ) embeddings = self.embedding_input_quantization(embeddings) return embeddings @@ -70,18 +86,21 @@ class QEmbedding(Embedding): """ def __init__( - self, - *args: int, - embedding_dim: int, - weight_quantization: Union[Quantization, str] = None, - output_quantization: Union[Quantization, str] = None, - **kwargs: int) -> None: + self, + *args: int, + embedding_dim: int, + weight_quantization: Union[Quantization, str] = None, + output_quantization: Union[Quantization, str] = None, + **kwargs: int, + ) -> None: super(QEmbedding, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore """load quantization functions""" self.embedding_weight_quantization = config.get_quantization_function( - weight_quantization or config.weight_quantization) + weight_quantization or config.weight_quantization + ) self.embedding_output_quantization = config.get_quantization_function( - output_quantization or config.input_quantization) + output_quantization or config.input_quantization + ) def forward(self, input: Tensor) -> Tensor: """generates embeddings for received bags. then quantizes these embeddings and depending on configuration @@ -94,9 +113,13 @@ def forward(self, input: Tensor) -> Tensor: Tensor: embeddings for given sequences """ embeddings = embedding( - input, self.embedding_weight_quantization(self.weight), self.padding_idx, - self.max_norm, self.norm_type, - self.scale_grad_by_freq, self.sparse, + input, + self.embedding_weight_quantization(self.weight), + self.padding_idx, + self.max_norm, + self.norm_type, + self.scale_grad_by_freq, + self.sparse, ) embeddings = self.embedding_output_quantization(embeddings) return embeddings diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 721fc29..f6c594f 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -15,12 +15,13 @@ class QLinearBase(Linear): def __init__( - self, - *args: int, - input_quantization: Union[str, Quantization] = None, - gradient_cancellation_threshold: Union[float, None] = None, - weight_quantization: Union[str, Quantization] = None, - **kwargs: bool) -> None: + self, + *args: int, + input_quantization: Union[str, Quantization] = None, + gradient_cancellation_threshold: Union[float, None] = None, + weight_quantization: Union[str, Quantization] = None, + **kwargs: bool, + ) -> None: """Applies the given quantization functions on weights and inputs before applying the linear operation. Args: @@ -85,4 +86,5 @@ class QLinear(DefaultImplementationMixin, QLinearBase): To implement a custom QLinear implementation use QLinearBase as a super class instead. """ + pass diff --git a/bitorch/layers/register.py b/bitorch/layers/register.py index 5900bd4..9577add 100644 --- a/bitorch/layers/register.py +++ b/bitorch/layers/register.py @@ -27,10 +27,7 @@ def all_layer_registries() -> List[LayerRegistry]: def convert_layers_to( - new_mode: RuntimeMode, - only: Optional[Iterable[Any]] = None, - device: torch.device = None, - verbose: bool = False + new_mode: RuntimeMode, only: Optional[Iterable[Any]] = None, device: torch.device = None, verbose: bool = False ) -> None: """ Convert all wrapped layers (or a given subset of them) to a new mode. @@ -48,6 +45,7 @@ class QLinearImplementation(LayerImplementation): """ Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. """ + def __init__(self, supports_modes: runtime_mode_type) -> None: """ Args: @@ -60,6 +58,7 @@ class QConv1dImplementation(LayerImplementation): """ Decorator for :class:`QConv1d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. """ + def __init__(self, supports_modes: runtime_mode_type) -> None: """ Args: @@ -72,6 +71,7 @@ class QConv2dImplementation(LayerImplementation): """ Decorator for :class:`QConv2d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. """ + def __init__(self, supports_modes: runtime_mode_type) -> None: """ Args: @@ -84,6 +84,7 @@ class QConv3dImplementation(LayerImplementation): """ Decorator for :class:`QConv3d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. """ + def __init__(self, supports_modes: runtime_mode_type) -> None: """ Args: diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index 06983d4..dd2e1d0 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -28,9 +28,20 @@ from ..util import build_lookup_dictionary __all__ = [ - "Model", "LeNet", "Resnet", "Resnet152V1", "Resnet152V2", "Resnet18V1", - "Resnet18V2", "Resnet34V1", "Resnet34V2", "Resnet50V1", "Resnet50V2", - "ResnetE", "ResnetE18", "ResnetE34", + "Model", + "LeNet", + "Resnet", + "Resnet152V1", + "Resnet152V2", + "Resnet18V1", + "Resnet18V2", + "Resnet34V1", + "Resnet34V2", + "Resnet50V1", + "Resnet50V2", + "ResnetE", + "ResnetE18", + "ResnetE34", ] diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 481513f..45375f9 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -7,12 +7,11 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, convert -from bitorch.layers.extensions.layer_container import LayerContainer -from bitorch.layers.register import q_linear_registry, q_conv1d_registry, q_conv2d_registry, q_conv3d_registry class Model(nn.Module): """Base class for Bitorch models""" + name = "None" def __init__(self, dataset: Union[BasicDataset, Type[BasicDataset]]) -> None: @@ -61,7 +60,7 @@ def initialize(self) -> None: else: if module.kernel_size[0] == 7: # first conv layer - nn.init.kaiming_normal_(module.weight, nonlinearity='relu') + nn.init.kaiming_normal_(module.weight, nonlinearity="relu") else: # other 32-bit conv layers nn.init.xavier_normal_(module.weight) diff --git a/bitorch/models/lenet.py b/bitorch/models/lenet.py index 9f9f67f..7dd2047 100644 --- a/bitorch/models/lenet.py +++ b/bitorch/models/lenet.py @@ -14,8 +14,13 @@ class LeNet(Model): num_fc = 1000 name = "lenet" - def generate_quant_model(self, weight_quant: str, input_quant: str, - weight_quant_2: str = None, input_quant_2: str = None) -> nn.Sequential: + def generate_quant_model( + self, + weight_quant: str, + input_quant: str, + weight_quant_2: str = None, + input_quant_2: str = None, + ) -> nn.Sequential: weight_quant_2 = weight_quant_2 or weight_quant input_quant_2 = input_quant_2 or input_quant @@ -24,25 +29,21 @@ def generate_quant_model(self, weight_quant: str, input_quant: str, self.activation_function(), nn.MaxPool2d(2, 2), nn.BatchNorm2d(self.num_channels_conv), - QConv2d( self.num_channels_conv, self.num_channels_conv, kernel_size=5, input_quantization=input_quant, - weight_quantization=weight_quant), + weight_quantization=weight_quant, + ), nn.BatchNorm2d(self.num_channels_conv), nn.MaxPool2d(2, 2), ShapePrintDebug(), - nn.Flatten(), - QActivation(activation=input_quant_2), - QLinear(self.num_channels_conv * 4 * 4, - self.num_fc, weight_quantization=weight_quant_2), + QLinear(self.num_channels_conv * 4 * 4, self.num_fc, weight_quantization=weight_quant_2), nn.BatchNorm1d(self.num_fc), self.activation_function(), - nn.Linear(self.num_fc, self.num_output), ) return model @@ -70,22 +71,17 @@ def __init__(self, dataset: BasicDataset, lenet_version: int = 0) -> None: nn.BatchNorm2d(self.num_channels_conv), self.activation_function(), nn.MaxPool2d(2, 2), - nn.Conv2d(self.num_channels_conv, self.num_channels_conv, kernel_size=5), nn.BatchNorm2d(self.num_channels_conv), self.activation_function(), nn.MaxPool2d(2, 2), - nn.Flatten(), - nn.Linear(self.num_channels_conv * 4 * 4, self.num_fc), nn.BatchNorm1d(self.num_fc), self.activation_function(), - nn.Linear(self.num_fc, self.num_output), ) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--lenet-version", type=int, default=0, - help="choses a verion of lenet") + parser.add_argument("--lenet-version", type=int, default=0, help="choose a version of lenet") diff --git a/bitorch/models/resnet.py b/bitorch/models/resnet.py index bb246f6..0337efd 100644 --- a/bitorch/models/resnet.py +++ b/bitorch/models/resnet.py @@ -111,8 +111,9 @@ def _build_downsampling(self) -> nn.Sequential: nn.Sequential: the downsampling model """ return nn.Sequential( - QConv2d_NoAct(self.in_channels, self.out_channels, kernel_size=1, - stride=self.stride, padding=0, bias=False), + QConv2d_NoAct( + self.in_channels, self.out_channels, kernel_size=1, stride=self.stride, padding=0, bias=False + ), nn.BatchNorm2d(self.out_channels), ) @@ -130,7 +131,7 @@ def _build_body(self) -> nn.Sequential: nn.BatchNorm2d(self.out_channels // 4), nn.ReLU(), QConv2d_NoAct(self.out_channels // 4, self.out_channels, kernel_size=1, stride=1), - nn.BatchNorm2d(self.out_channels) + nn.BatchNorm2d(self.out_channels), ) def forward(self, x: torch.Tensor) -> torch.Tensor: @@ -348,13 +349,14 @@ class ResNetV1(SpecificResnet): """ def __init__( - self, - block: Module, - layers: list, - channels: list, - classes: int, - initial_layers: str = "imagenet", - image_channels: int = 3) -> None: + self, + block: Module, + layers: list, + channels: list, + classes: int, + initial_layers: str = "imagenet", + image_channels: int = 3, + ) -> None: """Creates ResNetV1 model. Args: @@ -373,7 +375,8 @@ def __init__( super(ResNetV1, self).__init__(classes, channels) if len(channels) != (len(layers) + 1): raise ValueError( - f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!") + f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!" + ) feature_layers: List[nn.Module] = [] feature_layers.append(nn.BatchNorm2d(image_channels)) @@ -396,13 +399,14 @@ class ResNetV2(SpecificResnet): """ def __init__( - self, - block: Module, - layers: list, - channels: list, - classes: int = 1000, - initial_layers: str = "imagenet", - image_channels: int = 3) -> None: + self, + block: Module, + layers: list, + channels: list, + classes: int = 1000, + initial_layers: str = "imagenet", + image_channels: int = 3, + ) -> None: """Creates ResNetV2 model. Args: @@ -421,7 +425,8 @@ def __init__( super(ResNetV2, self).__init__(classes, channels) if len(channels) != (len(layers) + 1): raise ValueError( - f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!") + f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!" + ) feature_layers: List[nn.Module] = [] feature_layers.append(nn.BatchNorm2d(image_channels)) @@ -446,31 +451,38 @@ class Resnet(Model): name = "resnet" - resnet_spec = {18: ('basic_block', [2, 2, 2, 2], [64, 64, 128, 256, 512]), - 34: ('basic_block', [3, 4, 6, 3], [64, 64, 128, 256, 512]), - 50: ('bottle_neck', [3, 4, 6, 3], [64, 256, 512, 1024, 2048]), - 101: ('bottle_neck', [3, 4, 23, 3], [64, 256, 512, 1024, 2048]), - 152: ('bottle_neck', [3, 8, 36, 3], [64, 256, 512, 1024, 2048])} + resnet_spec = { + 18: ("basic_block", [2, 2, 2, 2], [64, 64, 128, 256, 512]), + 34: ("basic_block", [3, 4, 6, 3], [64, 64, 128, 256, 512]), + 50: ("bottle_neck", [3, 4, 6, 3], [64, 256, 512, 1024, 2048]), + 101: ("bottle_neck", [3, 4, 23, 3], [64, 256, 512, 1024, 2048]), + 152: ("bottle_neck", [3, 8, 36, 3], [64, 256, 512, 1024, 2048]), + } resnet_net_versions = [ResNetV1, ResNetV2] - resnet_block_versions = [{'basic_block': BasicBlockV1, 'bottle_neck': BottleneckV1}, - {'basic_block': BasicBlockV2, 'bottle_neck': BottleneckV2}] + resnet_block_versions = [ + {"basic_block": BasicBlockV1, "bottle_neck": BottleneckV1}, + {"basic_block": BasicBlockV2, "bottle_neck": BottleneckV2}, + ] - def __init__( - self, - resnet_version: int, - resnet_num_layers: int, - dataset: BasicDataset) -> None: + def __init__(self, resnet_version: int, resnet_num_layers: int, dataset: BasicDataset) -> None: super(Resnet, self).__init__(dataset) - self._model = self.create_resnet(resnet_version, resnet_num_layers, - self._dataset.num_classes, self._dataset.name, self._dataset.shape[1]) + self._model = self.create_resnet( + resnet_version, + resnet_num_layers, + self._dataset.num_classes, + self._dataset.name, + self._dataset.shape[1], + ) logging.info(f"building Resnetv{str(resnet_version)} with {str(resnet_num_layers)} layers...") - def create_resnet(self, - version: int, - num_layers: int, - classes: int = 1000, - initial_layers: str = "imagenet", - image_channels: int = 3) -> Module: + def create_resnet( + self, + version: int, + num_layers: int, + classes: int = 1000, + initial_layers: str = "imagenet", + image_channels: int = 3, + ) -> Module: """Creates a resnet complying to given version and layer number. Args: @@ -499,10 +511,20 @@ def create_resnet(self, @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--resnet-version", type=int, choices=[1, 2], required=True, - help="version of resnet to be used") - parser.add_argument("--resnet-num-layers", type=int, choices=[18, 34, 50, 152], required=True, - help="number of layers to be used inside resnet") + parser.add_argument( + "--resnet-version", + type=int, + choices=[1, 2], + required=True, + help="version of resnet to be used", + ) + parser.add_argument( + "--resnet-num-layers", + type=int, + choices=[18, 34, 50, 152], + required=True, + help="number of layers to be used inside resnet", + ) class Resnet18V1(Resnet): @@ -524,6 +546,7 @@ class Resnet34V1(Resnet): """ResNet-34 V1 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ + name = "resnet34v1" def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 30f71ac..4b7c4a9 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -13,7 +13,7 @@ from bitorch.layers import QConv2d from bitorch.models.common_layers import get_initial_layers -__all__ = ['ResnetE34', 'ResnetE18', 'ResnetE'] +__all__ = ["ResnetE34", "ResnetE18", "ResnetE"] class BasicBlock(nn.Module): @@ -54,8 +54,8 @@ def _build_downsampling(self) -> nn.Sequential: ) def _build_body(self) -> nn.Sequential: - """builds body of building block, i.e. two binary convolutions with batchnorms in between. Check referenced paper for - more details. + """Builds the body of a building block, i.e. two binary convolutions with BatchNorms in between. + Check the referenced paper for more details. Returns: nn.Sequential: the basic building block body model @@ -156,12 +156,8 @@ class _ResnetE(SpecificResnetE): """ def __init__( - self, - layers: list, - channels: list, - classes: int, - initial_layers: str = "imagenet", - image_channels: int = 3) -> None: + self, layers: list, channels: list, classes: int, initial_layers: str = "imagenet", image_channels: int = 3 + ) -> None: """Creates ResNetE model. Args: @@ -179,7 +175,8 @@ def __init__( super(_ResnetE, self).__init__(classes, channels) if len(channels) != (len(layers) + 1): raise ValueError( - f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!") + f"the len of channels ({len(channels)}) must be exactly the len of layers ({len(layers)}) + 1!" + ) feature_layers: List[nn.Module] = [] # feature_layers.append(nn.BatchNorm2d(image_channels, eps=2e-5, momentum=0.9)) @@ -204,25 +201,22 @@ class ResnetE(Model): name = "resnete" - resnet_spec = {18: ([2, 2, 2, 2], [64, 64, 128, 256, 512]), - 34: ([3, 4, 6, 3], [64, 64, 128, 256, 512])} + resnet_spec = { + 18: ([2, 2, 2, 2], [64, 64, 128, 256, 512]), + 34: ([3, 4, 6, 3], [64, 64, 128, 256, 512]), + } - def __init__( - self, - resnete_num_layers: int, - dataset: BasicDataset) -> None: + def __init__(self, resnete_num_layers: int, dataset: BasicDataset) -> None: super(ResnetE, self).__init__(dataset) - self._model = self.create(resnete_num_layers, self._dataset.num_classes, - self._dataset.name, self._dataset.shape[1]) + self._model = self.create( + resnete_num_layers, self._dataset.num_classes, self._dataset.name, self._dataset.shape[1] + ) logging.info(f"building ResnetE with {str(resnete_num_layers)} layers...") @classmethod def create( - cls, - num_layers: int, - classes: int = 1000, - initial_layers: str = "imagenet", - image_channels: int = 3) -> nn.Module: + cls, num_layers: int, classes: int = 1000, initial_layers: str = "imagenet", image_channels: int = 3 + ) -> nn.Module: """Creates a ResNetE complying to given layer number. Args: @@ -246,8 +240,13 @@ def create( @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--resnetE-num-layers", type=int, choices=[18, 34], required=True, - help="number of layers to be used inside resnetE") + parser.add_argument( + "--resnetE-num-layers", + type=int, + choices=[18, 34], + required=True, + help="number of layers to be used inside resnetE", + ) class ResnetE18(ResnetE): diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 5f5c8ee..535bcae 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -17,8 +17,14 @@ from ..util import build_lookup_dictionary __all__ = [ - "Quantization", "ApproxSign", "InputDoReFa", "WeightDoReFa", "Identity", "Sign", - "SteHeaviside", "SwishSign", + "Quantization", + "ApproxSign", + "InputDoReFa", + "WeightDoReFa", + "Identity", + "Sign", + "SteHeaviside", + "SwishSign", ] diff --git a/bitorch/quantizations/approx_sign.py b/bitorch/quantizations/approx_sign.py index 62c8840..4e5f2fb 100644 --- a/bitorch/quantizations/approx_sign.py +++ b/bitorch/quantizations/approx_sign.py @@ -12,8 +12,8 @@ class ApproxSignFunction(Function): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, input_tensor: torch.Tensor # type: ignore + ) -> torch.Tensor: """Binarize input tensor using the _sign function. Args: @@ -25,14 +25,14 @@ def forward( ctx.save_for_backward(input_tensor) sign_tensor = torch.sign(input_tensor) - sign_tensor = torch.where(sign_tensor == 0, torch.tensor(1., device=sign_tensor.device), sign_tensor) + sign_tensor = torch.where(sign_tensor == 0, torch.tensor(1.0, device=sign_tensor.device), sign_tensor) return sign_tensor @staticmethod @typing.no_type_check def backward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - output_grad: torch.Tensor) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, output_grad: torch.Tensor # type: ignore + ) -> torch.Tensor: """Apply approx sign function. used e.g. for birealnet Args: @@ -44,7 +44,7 @@ def backward( """ input_tensor = ctx.saved_tensors[0] # produces zeros where preactivation inputs exceeded threshold, ones otherwise - inside_threshold = (torch.abs(input_tensor) <= 1) + inside_threshold = torch.abs(input_tensor) <= 1 approx_sign = (2.0 - 2.0 * torch.abs(input_tensor)) * inside_threshold return approx_sign * output_grad diff --git a/bitorch/quantizations/base.py b/bitorch/quantizations/base.py index ffbf499..cf8f5db 100644 --- a/bitorch/quantizations/base.py +++ b/bitorch/quantizations/base.py @@ -14,8 +14,9 @@ class STE(Function): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + ) -> torch.Tensor: """just fowards the unchanged input_tensor. Args: @@ -54,8 +55,8 @@ def bitwidth(self) -> int: return self.bit_width def quantize(self, x: torch.Tensor) -> torch.Tensor: - """quantize the input tensor. It is recommended to use a torch.Function to also maniputlate backward behaiviour. See - the implementations of sign or dorefa quantization functions for more examples. + """quantize the input tensor. It is recommended to use a torch.Function to also maniputlate backward behavior. + See the implementations of sign or dorefa quantization functions for more examples. Args: x (torch.Tensor): the input to be quantized diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index 44de62f..86191ce 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -26,7 +26,7 @@ def __init__(self, bits: Union[int, None] = None) -> None: """ super(WeightDoReFa, self).__init__() self.bit_width = bits or config.dorefa_bits - self._max_value = 2 ** self.bit_width - 1 + self._max_value = 2**self.bit_width - 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """DoReFas the tensor to desired bit resolution using weight dorefa. @@ -47,8 +47,8 @@ class InputDoReFaFunction(Function): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor, bits: int) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, input_tensor: torch.Tensor, bits: int # type: ignore + ) -> torch.Tensor: """quantizes input tensor and forwards it. Args: @@ -59,7 +59,7 @@ def forward( Returns: torch.Tensor: the quantized input tensor """ - max_value = 2 ** bits - 1 + max_value = 2**bits - 1 quantized_tensor = torch.round(torch.clamp(input_tensor, 0, 1) * max_value) / max_value return quantized_tensor diff --git a/bitorch/quantizations/sign.py b/bitorch/quantizations/sign.py index ffa72d8..34ed4c5 100644 --- a/bitorch/quantizations/sign.py +++ b/bitorch/quantizations/sign.py @@ -10,8 +10,9 @@ class SignFunction(STE): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + ) -> torch.Tensor: """Binarize the input tensor using the sign function Args: @@ -22,8 +23,7 @@ def forward( torch.Tensor: the sign tensor """ sign_tensor = torch.sign(input_tensor) - sign_tensor = torch.where(sign_tensor == 0, torch.tensor( - 1., device=sign_tensor.device), sign_tensor) + sign_tensor = torch.where(sign_tensor == 0, torch.tensor(1.0, device=sign_tensor.device), sign_tensor) return sign_tensor diff --git a/bitorch/quantizations/ste_heaviside.py b/bitorch/quantizations/ste_heaviside.py index 052e355..e91651f 100644 --- a/bitorch/quantizations/ste_heaviside.py +++ b/bitorch/quantizations/ste_heaviside.py @@ -8,8 +8,9 @@ class SteHeavisideFunction(STE): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + ) -> torch.Tensor: """quantizes input tensor and forwards it. Args: @@ -20,8 +21,11 @@ def forward( torch.Tensor: the quantized input tensor """ - quantized_tensor = torch.where(input_tensor > 0, torch.tensor( - 1., device=input_tensor.device), torch.tensor(0., device=input_tensor.device)) + quantized_tensor = torch.where( + input_tensor > 0, + torch.tensor(1.0, device=input_tensor.device), + torch.tensor(0.0, device=input_tensor.device), + ) return quantized_tensor diff --git a/bitorch/quantizations/swish_sign.py b/bitorch/quantizations/swish_sign.py index 196cf99..7a90230 100644 --- a/bitorch/quantizations/swish_sign.py +++ b/bitorch/quantizations/swish_sign.py @@ -15,9 +15,10 @@ class SwishSignFunction(Function): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - input_tensor: torch.Tensor, - beta: float = 1.0) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + beta: float = 1.0, + ) -> torch.Tensor: """Binarize input tensor using the _sign function. Args: @@ -29,14 +30,14 @@ def forward( ctx.save_for_backward(input_tensor, torch.tensor(beta, device=input_tensor.device)) sign_tensor = torch.sign(input_tensor) - sign_tensor = torch.where(sign_tensor == 0, torch.tensor(1., device=input_tensor.device), sign_tensor) + sign_tensor = torch.where(sign_tensor == 0, torch.tensor(1.0, device=input_tensor.device), sign_tensor) return sign_tensor @staticmethod @typing.no_type_check def backward( - ctx: torch.autograd.function.BackwardCFunction, # type: ignore - output_grad: torch.Tensor) -> Tuple[torch.Tensor, None]: + ctx: torch.autograd.function.BackwardCFunction, output_grad: torch.Tensor # type: ignore + ) -> Tuple[torch.Tensor, None]: """Apply straight through estimator. This passes the output gradient as input gradient after clamping the gradient values to the range [-1, 1] @@ -50,8 +51,9 @@ def backward( """ input_tensor, beta = ctx.saved_tensors # produces zeros where preactivation inputs exceeded threshold, ones otherwise - swish = (beta * (2 - beta * input_tensor * torch.tanh(beta * input_tensor / 2))) / \ - (1 + torch.cosh(beta * input_tensor)) + swish = (beta * (2 - beta * input_tensor * torch.tanh(beta * input_tensor / 2))) / ( + 1 + torch.cosh(beta * input_tensor) + ) return swish * output_grad, None diff --git a/bitorch/runtime_mode.py b/bitorch/runtime_mode.py index 457be62..dd9a9d8 100644 --- a/bitorch/runtime_mode.py +++ b/bitorch/runtime_mode.py @@ -22,6 +22,7 @@ class RuntimeMode(Enum): - RAW: while in this mode, new layers are created as the default implementation BUT without wrapping, so they can not be switched to other layers later on (it does not influence already wrapped layers) """ + RAW = 0 DEFAULT = 1 CPU = 2 @@ -103,10 +104,10 @@ def __enter__(self) -> "_PauseWrapping": return self def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType] + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], ) -> None: bitorch.mode = self._previous_mode @@ -122,10 +123,10 @@ def __enter__(self) -> "_SafeModeChanger": return self def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType] + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], ) -> None: bitorch.mode = self._previous_mode diff --git a/bitorch/util.py b/bitorch/util.py index 157a9db..7a23fdc 100644 --- a/bitorch/util.py +++ b/bitorch/util.py @@ -6,11 +6,12 @@ @typing.no_type_check def build_lookup_dictionary( - current_module_name: str, - class_strings: List[str], - filter_by_superclass: Any = None, - filter_fn: Callable[[Any], bool] = None, - key_fn: Callable[[Any], str] = lambda x: x.name) -> Dict[str, Any]: + current_module_name: str, + class_strings: List[str], + filter_by_superclass: Any = None, + filter_fn: Callable[[Any], bool] = None, + key_fn: Callable[[Any], str] = lambda x: x.name, +) -> Dict[str, Any]: """Builds a lookup dictionary based on a list of strings of class names. Args: @@ -25,8 +26,10 @@ def build_lookup_dictionary( """ assert filter_fn is not None or filter_by_superclass is not None, "one of the filter options must be given" if filter_fn is None: + def filter_fn(x: Any) -> bool: return isinstance(x, type) and issubclass(x, filter_by_superclass) and x != filter_by_superclass + lookup = {} current_module = importlib.import_module(current_module_name) for class_name in class_strings: diff --git a/docs/source/conf.py b/docs/source/conf.py index 2e7c355..611c17d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,17 +12,18 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- -project = 'bitorch' -copyright = '2022, Joseph Bethge, Haojin Yang, Paul Mattes, Christopher Aust' -author = 'Joseph Bethge, Haojin Yang, Paul Mattes, Christopher Aust' +project = "bitorch" +copyright = "2022, Joseph Bethge, Haojin Yang, Paul Mattes, Christopher Aust" +author = "Joseph Bethge, Haojin Yang, Paul Mattes, Christopher Aust" # The full version, including alpha/beta/rc tags -release = 'v0.1' +release = "v0.1" # -- General configuration --------------------------------------------------- @@ -31,24 +32,24 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.napoleon', - 'nbsphinx', - 'nbsphinx_link', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.napoleon", + "nbsphinx", + "nbsphinx_link", ] # Generate type hints autodoc_typehints = "description" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] autosummary_generate = True @@ -63,7 +64,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 52a6110..544fe8e 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -4,7 +4,7 @@ Modified from the `PyTorch MNIST Example `_, which was published under the `BSD 3-Clause License `_. """ - +# fmt: off import argparse import torch diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 851237a..3578406 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,11 +1,13 @@ import os -if os.environ.get('REMOTE_PYCHARM_DEBUG_SESSION', False): + +if os.environ.get("REMOTE_PYCHARM_DEBUG_SESSION", False): import pydevd_pycharm + pydevd_pycharm.settrace( - 'localhost', - port=int(os.environ.get('REMOTE_PYCHARM_DEBUG_PORT', "12345")), + "localhost", + port=int(os.environ.get("REMOTE_PYCHARM_DEBUG_PORT", "12345")), stdoutToServer=True, - stderrToServer=True + stderrToServer=True, ) import argparse @@ -75,13 +77,19 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: project=args.wandb_project, log_model=True, name=args.wandb_experiment, - save_dir=str(output_dir) + save_dir=str(output_dir), ) # type: ignore ) callbacks = [] if args.checkpoint_dir is not None: - callbacks.append(ModelCheckpoint(args.checkpoint_dir, save_last=True, - save_top_k=args.checkpoint_keep_count, monitor="metrics/test-top1-accuracy")) + callbacks.append( + ModelCheckpoint( + args.checkpoint_dir, + save_last=True, + save_top_k=args.checkpoint_keep_count, + monitor="metrics/test-top1-accuracy", + ) + ) # providing our own progress bar disables the default progress bar (not needed to disable later on) cmd_logger = CommandLineLogger(args.log_interval) @@ -89,7 +97,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: configure_logging(cmd_logger.logger, args.log_file, args.log_level, args.log_stdout) if len(loggers) > 0: - lr_monitor = LearningRateMonitor(logging_interval='step') + lr_monitor = LearningRateMonitor(logging_interval="step") callbacks.append(lr_monitor) # type: ignore dataset = dataset_from_name(args.dataset) @@ -113,7 +121,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: max_steps=args.max_steps, logger=loggers if len(loggers) > 0 else None, # type: ignore callbacks=callbacks, # type: ignore - log_every_n_steps=args.log_interval + log_every_n_steps=args.log_interval, ) augmentation_level = Augmentation.from_string(args.augmentation) logger.info(f"model: {args.model}") @@ -128,17 +136,27 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level ) - train_loader = DataLoader(train_dataset, batch_size=args.batch_size, num_workers=args.num_workers, - shuffle=True, pin_memory=True, persistent_workers=True) # type: ignore - test_loader = DataLoader(test_dataset, batch_size=args.batch_size, num_workers=args.num_workers, - shuffle=False, pin_memory=True, persistent_workers=True) # type: ignore + train_loader = DataLoader( + train_dataset, + batch_size=args.batch_size, + num_workers=args.num_workers, + shuffle=True, + pin_memory=True, + persistent_workers=True, + ) # type: ignore + test_loader = DataLoader( + test_dataset, + batch_size=args.batch_size, + num_workers=args.num_workers, + shuffle=False, + pin_memory=True, + persistent_workers=True, + ) # type: ignore if FVBITCORE_AVAILABLE: data_point = torch.zeros(dataset.shape) computational_intensity = fv_nn.FlopCountAnalysis( - model, - inputs=data_point, - quantization_base_class=Quantization + model, inputs=data_point, quantization_base_class=Quantization ) stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) @@ -148,16 +166,18 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: total_flops = stats["#speed up flops (app.)"][""] logger.info("Approximated mflops: " + str(total_flops / 1e6)) if WANDB_AVAILABLE and args.wandb_log: - wandb.config.update({ - "mflops": total_flops / 1e6, - "size in MB": total_size / 1e6 / 8.0, - }) + wandb.config.update( + { + "mflops": total_flops / 1e6, + "size in MB": total_size / 1e6 / 8.0, + } + ) trainer.fit( model_wrapped, train_dataloaders=train_loader, val_dataloaders=test_loader, - ckpt_path=args.checkpoint_load if not args.pretrained else None + ckpt_path=args.checkpoint_load if not args.pretrained else None, ) diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 27ce8ef..e42daa6 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -15,40 +15,95 @@ def add_logging_args(parser: ArgumentParser) -> None: parser (ArgumentParser): the main argument parser """ log = parser.add_argument_group("Logging", "parameters for logging") - log.add_argument("--log-level", type=str, default="info", - choices=["debug", "info", "warning", "error", "critical"], - help="log level for logging message output",) - log.add_argument("--log-interval", type=int, default=100, metavar="N", - help="how many batches to wait before logging training status",) - log.add_argument("--log-file", type=str, default=None, - help="output file path for logging. default to stdout",) - log.add_argument("--log-stdout", action="store_true", - help="toggles force logging to stdout. if a log file is specified, logging will be" - "printed to both the log file and stdout",) - log.add_argument("--result-directory", type=str, default="./logs", - help="path to logs directory, e.g. tensorboard logs, csv files",) - log.add_argument("--disable-tensorboard-log", action="store_false", dest="tensorboard_log", - help="disables tensorboard logging",) - log.add_argument("--disable-csv-log", action="store_false", dest="csv_log", - help="disables csv logging",) - log.add_argument("--wandb", action="store_true", dest="wandb_log", - help="enables wandb logging (WANDB_API_KEY environment variable must be set)",) - log.add_argument("--wandb-project", type=str, default="bitorch", - help="name of wand project to be used by wandb logger",) - log.add_argument("--wandb-experiment", type=str, default=None, - help="name of wand experiment to be used by wandb logger",) + log.add_argument( + "--log-level", + type=str, + default="info", + choices=["debug", "info", "warning", "error", "critical"], + help="log level for logging message output", + ) + log.add_argument( + "--log-interval", + type=int, + default=100, + metavar="N", + help="how many batches to wait before logging training status", + ) + log.add_argument( + "--log-file", + type=str, + default=None, + help="output file path for logging. default to stdout", + ) + log.add_argument( + "--log-stdout", + action="store_true", + help="toggles force logging to stdout. if a log file is specified, logging will be" + "printed to both the log file and stdout", + ) + log.add_argument( + "--result-directory", + type=str, + default="./logs", + help="path to logs directory, e.g. tensorboard logs, csv files", + ) + log.add_argument( + "--disable-tensorboard-log", + action="store_false", + dest="tensorboard_log", + help="disables tensorboard logging", + ) + log.add_argument( + "--disable-csv-log", + action="store_false", + dest="csv_log", + help="disables csv logging", + ) + log.add_argument( + "--wandb", + action="store_true", + dest="wandb_log", + help="enables wandb logging (WANDB_API_KEY environment variable must be set)", + ) + log.add_argument( + "--wandb-project", + type=str, + default="bitorch", + help="name of wand project to be used by wandb logger", + ) + log.add_argument( + "--wandb-experiment", + type=str, + default=None, + help="name of wand experiment to be used by wandb logger", + ) def add_checkpoint_args(parser: ArgumentParser) -> None: checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") - checkpoint.add_argument("--checkpoint-dir", type=str, default=None, - help="set a custom path to store checkpoints in.",) - checkpoint.add_argument("--checkpoint-keep-count", type=int, default=1, - help="number of checkpoints to keep.",) - checkpoint.add_argument("--checkpoint-load", type=str, default=None, - help="path to checkpoint file to load state from. if omitted, a new model will be trained.",) - checkpoint.add_argument("--pretrained", action="store_true", - help="uses the given checkpoint as a pretrained model (only for initialization)",) + checkpoint.add_argument( + "--checkpoint-dir", + type=str, + default=None, + help="set a custom path to store checkpoints in.", + ) + checkpoint.add_argument( + "--checkpoint-keep-count", + type=int, + default=1, + help="number of checkpoints to keep.", + ) + checkpoint.add_argument( + "--checkpoint-load", + type=str, + default=None, + help="path to checkpoint file to load state from. if omitted, a new model will be trained.", + ) + checkpoint.add_argument( + "--pretrained", + action="store_true", + help="uses the given checkpoint as a pretrained model (only for initialization)", + ) def add_optimizer_args(parser: ArgumentParser) -> None: @@ -58,19 +113,43 @@ def add_optimizer_args(parser: ArgumentParser) -> None: parser (ArgumentParser): the main argument parser """ optimizer = parser.add_argument_group("Optimizer", "parameters for optimizer") - optimizer.add_argument("--lr-scheduler", type=str, - choices=["cosine", "step", "exponential"], - help="name of the lr scheduler to use. default to none",) - optimizer.add_argument("--lr", type=float, default=0.01, - help="initial learning rate (default: 0.01)",) - optimizer.add_argument('--lr-factor', default=0.1, type=float, - help='learning rate decay ratio. this is used only by the step and exponential lr scheduler',) - optimizer.add_argument('--lr-steps', nargs="*", default=[30, 60, 90], - help='list of learning rate decay epochs as list. this is used only by the step scheduler',) - optimizer.add_argument('--momentum', type=float, default=0.9, - help='momentum value for optimizer, default is 0.9. only used for sgd optimizer',) - optimizer.add_argument('--optimizer', type=str, default="adam", choices=["adam", "sgd", "radam"], - help='the optimizer to use. default is adam.',) + optimizer.add_argument( + "--lr-scheduler", + type=str, + choices=["cosine", "step", "exponential"], + help="name of the lr scheduler to use. default to none", + ) + optimizer.add_argument( + "--lr", + type=float, + default=0.01, + help="initial learning rate (default: 0.01)", + ) + optimizer.add_argument( + "--lr-factor", + default=0.1, + type=float, + help="learning rate decay ratio. this is used only by the step and exponential lr scheduler", + ) + optimizer.add_argument( + "--lr-steps", + nargs="*", + default=[30, 60, 90], + help="list of learning rate decay epochs as list. this is used only by the step scheduler", + ) + optimizer.add_argument( + "--momentum", + type=float, + default=0.9, + help="momentum value for optimizer, default is 0.9. only used for sgd optimizer", + ) + optimizer.add_argument( + "--optimizer", + type=str, + default="adam", + choices=["adam", "sgd", "radam"], + help="the optimizer to use. default is adam.", + ) def add_dataset_args(parser: ArgumentParser) -> None: @@ -80,21 +159,49 @@ def add_dataset_args(parser: ArgumentParser) -> None: parser (ArgumentParser): the main argument parser """ data = parser.add_argument_group("dataset", "parameters for the dataset used for training") - data.add_argument("--dataset", type=str, default="cifar10", choices=dataset_names(), - help="name of the dataset to be used for training",) - data.add_argument("--dataset-dir", type=str, default=None, - help="path to where the train dataset is saved / shall be downloaded to",) - data.add_argument("--download", action="store_true", - help="toggles wether the dataset shall be downloaded if not present. " - "only has effect with the cifar10 and mnist dataset so far.",) - data.add_argument("--batch-size", type=int, default=128, - help="batch size for training and testing (default: 128)",) - data.add_argument("--num-workers", type=int, default=4, - help="number of workers to be used for dataloading (default: 4)",) - data.add_argument("--augmentation", type=str, choices=["none", "low", "medium", "high"], default="none", - help="level of augmentation to be used in data preparation (default 'none')",) - data.add_argument("--fake-data", action="store_true", - help="train with fake data",) + data.add_argument( + "--dataset", + type=str, + default="cifar10", + choices=dataset_names(), + help="name of the dataset to be used for training", + ) + data.add_argument( + "--dataset-dir", + type=str, + default=None, + help="path to where the train dataset is saved / shall be downloaded to", + ) + data.add_argument( + "--download", + action="store_true", + help="toggles wether the dataset shall be downloaded if not present. " + "only has effect with the cifar10 and mnist dataset so far.", + ) + data.add_argument( + "--batch-size", + type=int, + default=128, + help="batch size for training and testing (default: 128)", + ) + data.add_argument( + "--num-workers", + type=int, + default=4, + help="number of workers to be used for dataloading (default: 4)", + ) + data.add_argument( + "--augmentation", + type=str, + choices=["none", "low", "medium", "high"], + default="none", + help="level of augmentation to be used in data preparation (default 'none')", + ) + data.add_argument( + "--fake-data", + action="store_true", + help="train with fake data", + ) def create_model_argparser(model_class: object) -> ArgumentParser: @@ -148,10 +255,18 @@ def add_regular_args(parser: ArgumentParser) -> None: add_config_args(parser) - parser.add_argument("--model", type=str.lower, choices=model_names(), required=True, - help="name of the model to be trained",) - parser.add_argument("--cpu", action="store_true", - help="explicitly use the cpu. overwrites gpu settings",) + parser.add_argument( + "--model", + type=str.lower, + choices=model_names(), + required=True, + help="name of the model to be trained", + ) + parser.add_argument( + "--cpu", + action="store_true", + help="explicitly use the cpu. overwrites gpu settings", + ) def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 0f3f806..19a7ce9 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -13,11 +13,12 @@ class ModelWrapper(LightningModule): def __init__( - self, - model: Module, - num_classes: int, - args: Namespace, - add_f1_prec_recall: bool = False) -> None: + self, + model: Module, + num_classes: int, + args: Namespace, + add_f1_prec_recall: bool = False, + ) -> None: super().__init__() self.save_hyperparameters(clean_hyperparameters(args)) self.loss_function = CrossEntropyLoss() @@ -39,10 +40,15 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore loss = self.loss_function(y_hat, y_train) self.train_accuracy_top1(y_hat, y_train) self.train_accuracy_top5(y_hat, y_train) - self.log_dict({ - "metrics/train-top1-accuracy": self.train_accuracy_top1, - "metrics/train-top5-accuracy": self.train_accuracy_top5, - }, prog_bar=True, on_step=True, on_epoch=False) + self.log_dict( + { + "metrics/train-top1-accuracy": self.train_accuracy_top1, + "metrics/train-top5-accuracy": self.train_accuracy_top5, + }, + prog_bar=True, + on_step=True, + on_epoch=False, + ) self.log("loss/train", loss, on_step=True, on_epoch=False) return loss @@ -65,11 +71,13 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: self.f1(y_hat, y_test) self.prec(y_hat, y_test) self.recall(y_hat, y_test) - metrics_dict.update({ - "metrics/f1": self.f1, - "metrics/precision": self.prec, - "metrics/recall": self.recall, - }) + metrics_dict.update( + { + "metrics/f1": self.f1, + "metrics/precision": self.prec, + "metrics/recall": self.recall, + } + ) self.log_dict(metrics_dict, prog_bar=True, on_step=False, on_epoch=True) return loss @@ -79,12 +87,12 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i optimizer = create_optimizer(self.hparams.optimizer, self.model, self.hparams.lr, self.hparams.momentum) if self.hparams.lr_scheduler is not None: scheduler = create_scheduler( - self.hparams.lr_scheduler, optimizer, self.hparams.lr_factor, - self.hparams.lr_steps, self.hparams.max_epochs + self.hparams.lr_scheduler, + optimizer, + self.hparams.lr_factor, + self.hparams.lr_steps, + self.hparams.max_epochs, ) - return { - "optimizer": optimizer, - "lr_scheduler": scheduler - } + return {"optimizer": optimizer, "lr_scheduler": scheduler} else: return optimizer diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index 8800f9c..b425775 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -9,11 +9,11 @@ TIME_INTERVALS = ( - ('w', 60 * 60 * 24 * 7), - ('d', 60 * 60 * 24), - ('h', 60 * 60), - ('m', 60), - ('s', 1), + ("w", 60 * 60 * 24 * 7), + ("d", 60 * 60 * 24), + ("h", 60 * 60), + ("m", 60), + ("s", 1), ) @@ -28,7 +28,7 @@ def display_time(seconds: float, granularity: int = 2) -> str: continue seconds -= value * count result.append(f"{value:02d}{name}") - return ':'.join(result[:granularity]) + return ":".join(result[:granularity]) class CommandLineLogger(ProgressBarBase): @@ -36,6 +36,7 @@ class CommandLineLogger(ProgressBarBase): This module provides a replacement for the default tqdm-based progress bar, that is more suitable for logging progress in a non-interactive way, e.g. to a file. """ + def __init__(self, refresh_rate: int) -> None: super().__init__() self._is_enabled = True diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/pytorch_lightning/utils/utils.py index 978d5b7..2a55bbb 100644 --- a/examples/pytorch_lightning/utils/utils.py +++ b/examples/pytorch_lightning/utils/utils.py @@ -23,8 +23,9 @@ def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, o logger.setLevel(log_level) logging_format = logging.Formatter( - '%(asctime)s - %(levelname)s [%(filename)s : %(funcName)s() : l. %(lineno)s]: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') + "%(asctime)s - %(levelname)s [%(filename)s : %(funcName)s() : l. %(lineno)s]: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) if log_file: log_file_path = Path(log_file) @@ -68,11 +69,12 @@ def create_optimizer(name: str, model: Module, lr: float, momentum: float) -> Op def create_scheduler( - scheduler_name: Optional[str], - optimizer: Optimizer, - lr_factor: float, - lr_steps: Optional[list], - epochs: int) -> Union[_LRScheduler, None]: + scheduler_name: Optional[str], + optimizer: Optimizer, + lr_factor: float, + lr_steps: Optional[list], + epochs: int, +) -> Union[_LRScheduler, None]: """creates a learning rate scheduler with the given parameters Args: diff --git a/setup.py b/setup.py index dda74ec..42e307a 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -import subprocess from pathlib import Path from typing import Union @@ -32,11 +31,11 @@ def get_requirements(file_path: Union[Path, str]): description="A package for building and training quantized and binary neural networks with Pytorch", long_description=readme_content, long_description_content_type="text/markdown", - packages=setuptools.find_packages(exclude='tests'), - install_requires=get_requirements('requirements.txt'), + packages=setuptools.find_packages(exclude="tests"), + install_requires=get_requirements("requirements.txt"), extras_require={ - "dev": get_requirements('requirements-dev.txt'), - "opt": get_requirements('requirements-opt.txt'), + "dev": get_requirements("requirements-dev.txt"), + "opt": get_requirements("requirements-opt.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", @@ -47,13 +46,16 @@ def get_requirements(file_path: Union[Path, str]): "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], - python_requires='>=3.7', + python_requires=">=3.7", data_files=[ - ('.', [ - 'version.txt', - 'requirements.txt', - 'requirements-dev.txt', - 'requirements-opt.txt', - ]), - ] + ( + ".", + [ + "version.txt", + "requirements.txt", + "requirements-dev.txt", + "requirements-opt.txt", + ], + ), + ], ) diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index 1ba0f70..a70c27b 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -35,6 +35,7 @@ def __init__(self, *args): @LayerImplementation(RuntimeMode.DEFAULT) class Layer(DefaultImplementationMixin, LayerBase): """Designate the LayerBase as the Default Mode""" + pass @@ -57,7 +58,7 @@ def class_name(self) -> str: return self.__class__.__name__ -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def clean_environment(): test_registry.clear() bitorch.mode = RuntimeMode.DEFAULT diff --git a/tests/layers/test_pact.py b/tests/layers/test_pact.py index a719f89..f11bb51 100644 --- a/tests/layers/test_pact.py +++ b/tests/layers/test_pact.py @@ -21,5 +21,5 @@ def test_qactivation(alpha): assert torch.equal(quantized, y) y.backward(x) - expected_gradient = torch.where((x >= 0) & (x <= alpha), x, torch.tensor(0.)) + expected_gradient = torch.where((x >= 0) & (x <= alpha), x, torch.tensor(0.0)) assert torch.equal(expected_gradient, x.grad) diff --git a/tests/layers/test_qactivation.py b/tests/layers/test_qactivation.py index 33ba071..b2c4085 100644 --- a/tests/layers/test_qactivation.py +++ b/tests/layers/test_qactivation.py @@ -29,7 +29,7 @@ def test_q_activation(threshold): y.backward(x) if threshold > 0: - expected_gradient = torch.where(torch.abs(x) <= threshold, x, torch.tensor(0.)) + expected_gradient = torch.where(torch.abs(x) <= threshold, x, torch.tensor(0.0)) else: expected_gradient = x.clone() assert torch.equal(expected_gradient, x.grad) diff --git a/tests/layers/test_qconv.py b/tests/layers/test_qconv.py index b523471..ecee778 100644 --- a/tests/layers/test_qconv.py +++ b/tests/layers/test_qconv.py @@ -6,21 +6,66 @@ import numpy as np TEST_INPUT_DATA = [ - (QConv1d, conv1d, (1, 2, 5), [2, 2], - {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}), - (QConv2d, conv2d, (1, 2, 5, 5), [2, 2], - {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}), - (QConv3d, conv3d, (1, 2, 4, 4, 4), [2, 2], - {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}), - (QConv1d, conv1d, (1, 2, 5), [2, 2], - {"kernel_size": 3, "weight_quantization": Sign(), "input_quantization": "sign", - "gradient_cancellation_threshold": 0.5, "padding": 1}), - (QConv2d, conv2d, (1, 2, 5, 5), [2, 2], - {"kernel_size": 3, "weight_quantization": Sign(), "input_quantization": "sign", - "gradient_cancellation_threshold": 1.0, "padding": 1}), - (QConv3d, conv3d, (1, 2, 4, 4, 4), [2, 2], - {"kernel_size": 3, "weight_quantization": Sign(), "input_quantization": "sign", - "gradient_cancellation_threshold": 2.0, "padding": 1}), + ( + QConv1d, + conv1d, + (1, 2, 5), + [2, 2], + {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}, + ), + ( + QConv2d, + conv2d, + (1, 2, 5, 5), + [2, 2], + {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}, + ), + ( + QConv3d, + conv3d, + (1, 2, 4, 4, 4), + [2, 2], + {"kernel_size": 3, "weight_quantization": "sign", "input_quantization": "sign", "padding": 1}, + ), + ( + QConv1d, + conv1d, + (1, 2, 5), + [2, 2], + { + "kernel_size": 3, + "weight_quantization": Sign(), + "input_quantization": "sign", + "gradient_cancellation_threshold": 0.5, + "padding": 1, + }, + ), + ( + QConv2d, + conv2d, + (1, 2, 5, 5), + [2, 2], + { + "kernel_size": 3, + "weight_quantization": Sign(), + "input_quantization": "sign", + "gradient_cancellation_threshold": 1.0, + "padding": 1, + }, + ), + ( + QConv3d, + conv3d, + (1, 2, 4, 4, 4), + [2, 2], + { + "kernel_size": 3, + "weight_quantization": Sign(), + "input_quantization": "sign", + "gradient_cancellation_threshold": 2.0, + "padding": 1, + }, + ), ] * 10 @@ -50,7 +95,8 @@ def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): stride=layer.stride, padding=0, dilation=layer.dilation, - groups=layer.groups) + groups=layer.groups, + ) direct_result.backward(input_tensor) grad2 = input_tensor.grad.clone() @@ -59,20 +105,23 @@ def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): assert torch.equal(grad1, grad2) -@pytest.mark.parametrize("all_args", [ +@pytest.mark.parametrize( + "all_args", [ - ("in_channels", 16), - ("out_channels", 64), - ("kernel_size", 3), - ("stride", 1), - ("padding", 1), - ("dilation", 1), - ("groups", 1), - ("bias", True), - ("padding_mode", "zeros"), - ("device", None), - ("dtype", None), + [ + ("in_channels", 16), + ("out_channels", 64), + ("kernel_size", 3), + ("stride", 1), + ("padding", 1), + ("dilation", 1), + ("groups", 1), + ("bias", True), + ("padding_mode", "zeros"), + ("device", None), + ("dtype", None), + ], ], -]) +) def test_args_function(all_args): pass diff --git a/tests/layers/test_qconv_noact.py b/tests/layers/test_qconv_noact.py index 0d4769c..a7e146f 100644 --- a/tests/layers/test_qconv_noact.py +++ b/tests/layers/test_qconv_noact.py @@ -11,8 +11,7 @@ (QConv3d_NoAct, conv3d, (1, 2, 4, 4, 4), [2, 2], {"kernel_size": 3, "weight_quantization": "sign", "padding": 1}), (QConv1d_NoAct, conv1d, (1, 2, 5), [2, 2], {"kernel_size": 3, "weight_quantization": Sign(), "padding": 1}), (QConv2d_NoAct, conv2d, (1, 2, 5, 5), [2, 2], {"kernel_size": 3, "weight_quantization": Sign(), "padding": 1}), - (QConv3d_NoAct, conv3d, (1, 2, 4, 4, 4), [2, 2], { - "kernel_size": 3, "weight_quantization": Sign(), "padding": 1}), + (QConv3d_NoAct, conv3d, (1, 2, 4, 4, 4), [2, 2], {"kernel_size": 3, "weight_quantization": Sign(), "padding": 1}), ] * 10 @@ -42,7 +41,8 @@ def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): stride=layer.stride, padding=0, dilation=layer.dilation, - groups=layer.groups) + groups=layer.groups, + ) direct_result.backward(input_tensor) grad2 = input_tensor.grad.clone() diff --git a/tests/layers/test_qembeddings.py b/tests/layers/test_qembeddings.py index b3828bf..a08fa8a 100644 --- a/tests/layers/test_qembeddings.py +++ b/tests/layers/test_qembeddings.py @@ -17,9 +17,13 @@ @pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) @pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) def test_qembedding(vocab_size, embedding_size, quantization_function): - qembedding = QEmbedding(num_embeddings=vocab_size, embedding_dim=embedding_size, - weight_quantization=quantization_function(), - output_quantization=quantization_function(), sparse=False) + qembedding = QEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + output_quantization=quantization_function(), + sparse=False, + ) example_input = torch.zeros(vocab_size, dtype=int) example_input[np.random.randint(vocab_size)] = 1 @@ -30,16 +34,24 @@ def test_qembedding(vocab_size, embedding_size, quantization_function): binarized_embedding_table = quantization(qembedding.weight) raw_embeddings = embedding( - example_input, binarized_embedding_table, qembedding.padding_idx, - qembedding.max_norm, qembedding.norm_type, - qembedding.scale_grad_by_freq, False, + example_input, + binarized_embedding_table, + qembedding.padding_idx, + qembedding.max_norm, + qembedding.norm_type, + qembedding.scale_grad_by_freq, + False, ) assert torch.equal(output, quantization(raw_embeddings)) # now sparse tests - qembedding = QEmbedding(num_embeddings=vocab_size, embedding_dim=embedding_size, - weight_quantization=quantization_function(), - output_quantization=quantization_function(), sparse=True) + qembedding = QEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + output_quantization=quantization_function(), + sparse=True, + ) example_input = torch.tensor(np.random.randint(vocab_size), dtype=int) @@ -48,9 +60,13 @@ def test_qembedding(vocab_size, embedding_size, quantization_function): binarized_embedding_table = quantization(qembedding.weight) raw_embeddings = embedding( - example_input, binarized_embedding_table, qembedding.padding_idx, - qembedding.max_norm, qembedding.norm_type, - qembedding.scale_grad_by_freq, True, + example_input, + binarized_embedding_table, + qembedding.padding_idx, + qembedding.max_norm, + qembedding.norm_type, + qembedding.scale_grad_by_freq, + True, ) assert torch.equal(output, quantization(raw_embeddings)) @@ -59,14 +75,19 @@ def test_qembedding(vocab_size, embedding_size, quantization_function): @pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) def test_qembeddingbag(vocab_size, embedding_size, quantization_function): - qembeddingbag = QEmbeddingBag(num_embeddings=vocab_size, embedding_dim=embedding_size, - weight_quantization=quantization_function(), - output_quantization=quantization_function(), sparse=False, mode="mean") + qembeddingbag = QEmbeddingBag( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + output_quantization=quantization_function(), + sparse=False, + mode="mean", + ) example_input = torch.zeros(vocab_size, dtype=int) for _ in range(np.random.randint(vocab_size)): example_input[np.random.randint(vocab_size)] = 1 - example_offset = torch.tensor((0, ), dtype=int) + example_offset = torch.tensor((0,), dtype=int) quantization = quantization_function() output = qembeddingbag(example_input, example_offset) @@ -74,28 +95,45 @@ def test_qembeddingbag(vocab_size, embedding_size, quantization_function): binarized_embedding_table = quantization(qembeddingbag.weight) # necessary for torch 1.8 compliance - if hasattr(qembeddingbag, 'padding_idx'): + if hasattr(qembeddingbag, "padding_idx"): raw_embeddings = embedding_bag( - example_input, binarized_embedding_table, example_offset, - qembeddingbag.max_norm, qembeddingbag.norm_type, - qembeddingbag.scale_grad_by_freq, qembeddingbag.mode, False, - None, qembeddingbag.include_last_offset, - qembeddingbag.padding_idx + example_input, + binarized_embedding_table, + example_offset, + qembeddingbag.max_norm, + qembeddingbag.norm_type, + qembeddingbag.scale_grad_by_freq, + qembeddingbag.mode, + False, + None, + qembeddingbag.include_last_offset, + qembeddingbag.padding_idx, ) else: raw_embeddings = embedding_bag( - example_input, binarized_embedding_table, example_offset, - qembeddingbag.max_norm, qembeddingbag.norm_type, - qembeddingbag.scale_grad_by_freq, qembeddingbag.mode, False, - None, qembeddingbag.include_last_offset, + example_input, + binarized_embedding_table, + example_offset, + qembeddingbag.max_norm, + qembeddingbag.norm_type, + qembeddingbag.scale_grad_by_freq, + qembeddingbag.mode, + False, + None, + qembeddingbag.include_last_offset, ) assert torch.equal(output, quantization(raw_embeddings)) # now sparse tests - qembeddingbag = QEmbeddingBag(num_embeddings=vocab_size, embedding_dim=embedding_size, - weight_quantization=quantization_function(), - output_quantization=quantization_function(), sparse=True, mode="mean") + qembeddingbag = QEmbeddingBag( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + output_quantization=quantization_function(), + sparse=True, + mode="mean", + ) example_input = torch.tensor(np.random.randint(vocab_size, size=np.random.randint(vocab_size)), dtype=int) @@ -104,20 +142,32 @@ def test_qembeddingbag(vocab_size, embedding_size, quantization_function): binarized_embedding_table = quantization(qembeddingbag.weight) # necessary for torch 1.8 compliance - if hasattr(qembeddingbag, 'padding_idx'): + if hasattr(qembeddingbag, "padding_idx"): raw_embeddings = embedding_bag( - example_input, binarized_embedding_table, example_offset, - qembeddingbag.max_norm, qembeddingbag.norm_type, - qembeddingbag.scale_grad_by_freq, qembeddingbag.mode, True, - None, qembeddingbag.include_last_offset, - qembeddingbag.padding_idx + example_input, + binarized_embedding_table, + example_offset, + qembeddingbag.max_norm, + qembeddingbag.norm_type, + qembeddingbag.scale_grad_by_freq, + qembeddingbag.mode, + True, + None, + qembeddingbag.include_last_offset, + qembeddingbag.padding_idx, ) else: raw_embeddings = embedding_bag( - example_input, binarized_embedding_table, example_offset, - qembeddingbag.max_norm, qembeddingbag.norm_type, - qembeddingbag.scale_grad_by_freq, qembeddingbag.mode, True, - None, qembeddingbag.include_last_offset, + example_input, + binarized_embedding_table, + example_offset, + qembeddingbag.max_norm, + qembeddingbag.norm_type, + qembeddingbag.scale_grad_by_freq, + qembeddingbag.mode, + True, + None, + qembeddingbag.include_last_offset, ) output = torch.nan_to_num(output, nan=0.0) output_raw = torch.nan_to_num(quantization(raw_embeddings), nan=0.0) diff --git a/tests/layers/test_qlinear.py b/tests/layers/test_qlinear.py index ce307f6..1fb2172 100644 --- a/tests/layers/test_qlinear.py +++ b/tests/layers/test_qlinear.py @@ -6,13 +6,7 @@ from bitorch.layers.qlinear import QLinear, QLinearBase from bitorch.quantizations import Sign, quantization_from_name -TEST_INPUT_DATA = [ - [0., 0.], - [1., 0.], - [-1., 1.], - [0.3, -0.3], - [1e12, -1e12] -] +TEST_INPUT_DATA = [[0.0, 0.0], [1.0, 0.0], [-1.0, 1.0], [0.3, -0.3], [1e12, -1e12]] @pytest.mark.parametrize("input_values", TEST_INPUT_DATA) @@ -36,18 +30,21 @@ def test_qlinear(input_values, quantization): assert torch.equal(result, y) -@pytest.mark.parametrize("all_args", [ +@pytest.mark.parametrize( + "all_args", [ - ("in_features", 64), - ("out_features", 32), - ("input_quantization", Sign()), - ("gradient_cancellation_threshold", 1.3), - ("weight_quantization", Sign()), - ("bias", False), - ("device", None), - ("dtype", None), + [ + ("in_features", 64), + ("out_features", 32), + ("input_quantization", Sign()), + ("gradient_cancellation_threshold", 1.3), + ("weight_quantization", Sign()), + ("bias", False), + ("device", None), + ("dtype", None), + ], ], -]) +) def test_args_function(all_args): positional_args = 2 diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index a209364..66a1ddc 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -14,8 +14,14 @@ from bitorch.layers.extensions import LayerRecipe from bitorch.layers.qconv2d import QConv2dBase from bitorch.layers.qlinear import QLinearBase -from bitorch.layers.register import q_linear_registry, QLinearImplementation, q_conv1d_registry, q_conv2d_registry, \ - QConv2dImplementation, q_conv3d_registry +from bitorch.layers.register import ( + q_linear_registry, + QLinearImplementation, + q_conv1d_registry, + q_conv2d_registry, + QConv2dImplementation, + q_conv3d_registry, +) from bitorch.models import Model TEST_MODE = RuntimeMode.INFERENCE_AUTO @@ -93,6 +99,7 @@ def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer._layer.weight = recipe.layer.weight new_layer._layer.bias = recipe.layer.bias return new_layer + yield QLinearTestImpl, QConv2dTestImpl reset() @@ -134,6 +141,7 @@ def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: new_layer.weight = recipe.layer.weight new_layer.bias = recipe.layer.bias return new_layer + yield QLinearTestImpl, QConv2dTestImpl reset() diff --git a/tests/models/test_models.py b/tests/models/test_models.py index bd2eed9..0a89a81 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -12,7 +12,7 @@ Resnet50V2, ResnetE, ResnetE18, - ResnetE34 + ResnetE34, ) import torch import numpy as np @@ -33,7 +33,7 @@ [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], [ResnetE18, {}, RGB_DATASETS], [ResnetE34, {}, RGB_DATASETS], - [LeNet, {"lenet_version": [0,1,2,3,4]}, [MNIST]], + [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], ] @@ -59,5 +59,6 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: model = model_class(dataset=dataset, **combination) input_values = torch.Tensor(np.random.uniform(0, 1.0, input_shape)) output = model(input_values) - assert torch.equal(torch.as_tensor(output.shape), torch.Tensor( - [input_shape[0], dataset.num_classes]).long()) + assert torch.equal( + torch.as_tensor(output.shape), torch.Tensor([input_shape[0], dataset.num_classes]).long() + ) diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index 1aa97de..da3d667 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -8,36 +8,75 @@ ApproxSign, SteHeaviside, SwishSign, - quantization_from_name + quantization_from_name, ) TEST_INPUT_DATA = [ - (Sign(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (ApproxSign(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], - [0.0, 0.0, 1.4, 2.0, 1.4, 0.0, 0.0]), - (SteHeaviside(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], - [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (SwishSign(5.0), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], - [-0.03, -0.195, 1.562, 5.0, 1.562, -0.195, -0.03]), - (InputDoReFa(bits=2), 2, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (WeightDoReFa(bits=2), 2, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], - [-1.0, -1.0, -1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - (InputDoReFa(bits=1), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), - (WeightDoReFa(bits=1), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], - [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ( + Sign(), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + ApproxSign(), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 1.4, 2.0, 1.4, 0.0, 0.0], + ), + ( + SteHeaviside(), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + SwishSign(5.0), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], + [-0.03, -0.195, 1.562, 5.0, 1.562, -0.195, -0.03], + ), + ( + InputDoReFa(bits=2), + 2, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + WeightDoReFa(bits=2), + 2, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + ), + ( + InputDoReFa(bits=1), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + WeightDoReFa(bits=1), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + ), ] -@pytest.mark.parametrize("quantization, bits, input_values, expected_output, expected_gradient_factors", TEST_INPUT_DATA) +@pytest.mark.parametrize( + "quantization, bits, input_values, expected_output, expected_gradient_factors", TEST_INPUT_DATA +) def test_quantizations( - quantization: Quantization, - bits: int, - input_values: list, - expected_output: list, - expected_gradient_factors: list) -> None: + quantization: Quantization, bits: int, input_values: list, expected_output: list, expected_gradient_factors: list +) -> None: x = torch.tensor(input_values).float().requires_grad_(True) x_exp = torch.tensor(expected_output).float().requires_grad_(True) exp_grad_factors = torch.tensor(expected_gradient_factors).float().requires_grad_(True) From e97340a2c7ab8de30015078962ec38facd436cc7 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Wed, 22 Jun 2022 21:30:23 +0200 Subject: [PATCH 053/208] move optional requirements into examples folder and skip tests that need imports --- examples/requirements.txt | 6 ++++++ requirements-opt.txt | 2 -- requirements.txt | 4 ---- setup.py | 3 +-- tests/test_argparse.py | 10 +++++----- 5 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 examples/requirements.txt delete mode 100644 requirements-opt.txt diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..b4b1f22 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,6 @@ +wandb +fvbitcore +sklearn +pytorch_lightning +protobuf~=3.20.0 +torchmetrics \ No newline at end of file diff --git a/requirements-opt.txt b/requirements-opt.txt deleted file mode 100644 index 3969cbf..0000000 --- a/requirements-opt.txt +++ /dev/null @@ -1,2 +0,0 @@ -wandb -fvbitcore diff --git a/requirements.txt b/requirements.txt index 7334424..984439e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,3 @@ torchvision bitorchinfo matplotlib numpy -sklearn -pytorch_lightning -protobuf~=3.20.0 -torchmetrics diff --git a/setup.py b/setup.py index 42e307a..1e887fe 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def get_requirements(file_path: Union[Path, str]): install_requires=get_requirements("requirements.txt"), extras_require={ "dev": get_requirements("requirements-dev.txt"), - "opt": get_requirements("requirements-opt.txt"), + "opt": get_requirements("examples/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", @@ -54,7 +54,6 @@ def get_requirements(file_path: Union[Path, str]): "version.txt", "requirements.txt", "requirements-dev.txt", - "requirements-opt.txt", ], ), ], diff --git a/tests/test_argparse.py b/tests/test_argparse.py index c648f9e..5de14bb 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -1,10 +1,10 @@ +import pytest from argparse import ArgumentParser -from examples.pytorch_lightning.utils.arg_parser import add_regular_args, add_all_model_args - -# this test checks for naming conflicts by adding all arguments to one parser +# this test checks for naming conflicts by adding all arguments to one parser def test_argparse(): + arg_parser = pytest.importorskip("examples.pytorch_lightning.utils.arg_parser") parser = ArgumentParser() - add_regular_args(parser) - add_all_model_args(parser) + arg_parser.add_regular_args(parser) + arg_parser.add_all_model_args(parser) From 1e2e85d7d8b9127800fb9927743820d7f297efcd Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 09:08:37 +0200 Subject: [PATCH 054/208] add bash script for convenient code style checking --- README.md | 25 +++++++++++++++---------- check-codestyle.sh | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) create mode 100755 check-codestyle.sh diff --git a/README.md b/README.md index 22b9010..ff892fe 100644 --- a/README.md +++ b/README.md @@ -66,38 +66,43 @@ Install the package and _dev_ requirements locally for development: pip install -e ".[dev]" ``` +### Tests + +The tests can be run with [pytest](https://docs.pytest.org/): + +```bash +pytest +``` + ### Code formatting and typing -New code should be compatible with Python 3.X versions and be compliant with PEP8. To check the codebase, please run +For conveniently checking whether your code suites the required style (more details below), run +```bash +./check-codestyle.sh +``` +New code should be compatible with Python 3.X versions and be compliant with PEP8. To check the codebase, please run ```bash flake8 ``` The codebase has type annotations, please make sure to add type hints if required. We use `mypy` for type checking: - ```bash mypy --config-file mypy.ini ``` - For code formatting we use `black`: ```bash black . --check --verbose --diff --color # check what changes the formatter would do black . # apply the formatter ``` -In order to automatically apply the code formatting with every commit, you can install pre-commit and use the pre-commit hook: +In order to automatically apply the code formatting with every commit, you can also install pre-commit +and use the pre-commit hook: ```bash pre-commit install ``` -Finally, the tests can be run with: - -```bash -pytest -``` - ### Documentation We use [Google's Python Docstring Format](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) diff --git a/check-codestyle.sh b/check-codestyle.sh new file mode 100755 index 0000000..4b0370e --- /dev/null +++ b/check-codestyle.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +fails=() +successes=() + +checkmark="✔" +cross="✘" + +function check() { + echo "+ $@" + "$@" && { + successes+=("${checkmark} ${1}") + } || { + fails+=("${cross} ${1}") + } +} + +check flake8 +check mypy --config-file mypy.ini +check black . --check --diff --color + +echo +if [ "${#successes[@]}" -gt "0" ]; then + echo "Successful checks:" + echo ${successes[@]} +fi +if [ "${#fails[@]}" -gt "0" ]; then + echo "The following checks failed (please check the output above):" + echo ${fails[@]} +else + echo + echo "All looking good!" +fi From 8eed9a981c18c9cb46dc1db490f3a7691e35aaff Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 09:09:42 +0200 Subject: [PATCH 055/208] add return code to script --- check-codestyle.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/check-codestyle.sh b/check-codestyle.sh index 4b0370e..9ff5a44 100755 --- a/check-codestyle.sh +++ b/check-codestyle.sh @@ -27,6 +27,7 @@ fi if [ "${#fails[@]}" -gt "0" ]; then echo "The following checks failed (please check the output above):" echo ${fails[@]} + exit 1 else echo echo "All looking good!" From ed289a6f34968092558453f0ef1431d8064cfb31 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 09:32:18 +0200 Subject: [PATCH 056/208] move requirements into examples --- examples/mnist/requirements.txt | 1 + examples/{ => pytorch_lightning}/requirements.txt | 3 +-- requirements-dev.txt | 2 +- requirements.txt | 1 - setup.py | 3 ++- 5 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 examples/mnist/requirements.txt rename examples/{ => pytorch_lightning}/requirements.txt (59%) diff --git a/examples/mnist/requirements.txt b/examples/mnist/requirements.txt new file mode 100644 index 0000000..99f86ab --- /dev/null +++ b/examples/mnist/requirements.txt @@ -0,0 +1 @@ +bitorch_inference_engine diff --git a/examples/requirements.txt b/examples/pytorch_lightning/requirements.txt similarity index 59% rename from examples/requirements.txt rename to examples/pytorch_lightning/requirements.txt index b4b1f22..38e59b2 100644 --- a/examples/requirements.txt +++ b/examples/pytorch_lightning/requirements.txt @@ -2,5 +2,4 @@ wandb fvbitcore sklearn pytorch_lightning -protobuf~=3.20.0 -torchmetrics \ No newline at end of file +torchmetrics diff --git a/requirements-dev.txt b/requirements-dev.txt index d96fd6c..ebd80fb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,4 +10,4 @@ pep8-naming pytest sphinx twine -pre-commit \ No newline at end of file +pre-commit diff --git a/requirements.txt b/requirements.txt index 984439e..1017d57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ torch torchvision -bitorchinfo matplotlib numpy diff --git a/setup.py b/setup.py index 1e887fe..9662b0d 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ def get_requirements(file_path: Union[Path, str]): install_requires=get_requirements("requirements.txt"), extras_require={ "dev": get_requirements("requirements-dev.txt"), - "opt": get_requirements("examples/requirements.txt"), + "opt": get_requirements("examples/pytorch_lightning/requirements.txt") + + get_requirements("examples/mnist/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", From bd60acc5ba419cb164088b37c32b4eea152c4f17 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 10:26:12 +0200 Subject: [PATCH 057/208] update readmes about requirements --- README.md | 2 +- bitorch/models/base.py | 17 ++++- examples/mnist/README.md | 8 +- examples/pytorch_lightning/README.md | 7 +- .../pytorch_lightning/image_classification.py | 75 +++++++------------ 5 files changed, 57 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 22b9010..d824b43 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Note, that you can also request a specific PyTorch version directly, e.g. for CU pip install bitorch --extra-index-url https://download.pytorch.org/whl/cu113 ``` -If you want to be able to run the examples, install the optional dependencies as well: +If you want to run the examples install the optional dependencies as well: ```bash pip install "bitorch[opt]" diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 45375f9..8da4e16 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -6,7 +6,10 @@ from bitorch import RuntimeMode from bitorch.datasets.base import BasicDataset -from bitorch.layers import QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, convert +from bitorch.layers import convert +from bitorch.layers.qconv1d import QConv1dBase, QConv1d_NoAct +from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct +from bitorch.layers.qconv3d import QConv3dBase, QConv3d_NoAct class Model(nn.Module): @@ -55,7 +58,17 @@ def initialize(self) -> None: for module in self._model.modules(): if isinstance(module, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): # binary layers - if isinstance(module, (QConv1d, QConv2d, QConv3d, QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct)): + if isinstance( + module, + ( + QConv1dBase, + QConv2dBase, + QConv3dBase, + QConv1d_NoAct, + QConv2d_NoAct, + QConv3d_NoAct, + ), + ): nn.init.xavier_normal_(module.weight) else: if module.kernel_size[0] == 7: diff --git a/examples/mnist/README.md b/examples/mnist/README.md index cc566c9..86aa016 100644 --- a/examples/mnist/README.md +++ b/examples/mnist/README.md @@ -2,7 +2,13 @@ In this example script we train a simple model for the MNIST dataset and also use the [bitorch inference engine](https://github.com/hpi-xnor/bitorch-inference-engine) for speed up. -For example, run the following to train an MLP with 3 layers (one of which is a binary layer), +First the requirements for this example need to be installed +(unless the optional dependencies of BITorch were already installed): +```bash +pip install -r requirements.txt +``` + +Then you can run the following to train an MLP with 3 layers (one of which is a binary layer), or add `--help` for more arguments: ```bash python train_mnist.py --epochs 10 --model mlp --log-interval 100 diff --git a/examples/pytorch_lightning/README.md b/examples/pytorch_lightning/README.md index a0eaeba..9b4d4cd 100644 --- a/examples/pytorch_lightning/README.md +++ b/examples/pytorch_lightning/README.md @@ -3,8 +3,13 @@ To give an example on how to use bitorch for your own projects `image_classification.py` trains one of the models implemented in `bitorch` on an image classification dataset. -Below you can find an example call of the script: +First the requirements for this example need to be installed +(unless the optional dependencies of BITorch were already installed): +```bash +pip install -r requirements.txt +``` +Below you can find an example call of the script: ```bash python3 image_classification.py --optimizer adam --lr 0.001 --lr-scheduler cosine --max_epochs 2 --dataset imagenet --model resnet18v1 --batch-size 128 --accelerator gpu --num-workers 16 --gpus 3 ``` diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 3578406..06d7a6a 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -15,43 +15,27 @@ from pathlib import Path from typing import List +import fvbitcore.nn as fv_nn import torch - -from torch.utils.data import DataLoader +import wandb from pytorch_lightning import Trainer from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor -from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase -from utils.utils import configure_logging -from utils.arg_parser import create_argparser -from utils.lightning_model import ModelWrapper +from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase, WandbLogger +from torch.utils.data import DataLoader +from bitorch import apply_args_to_configuration +from bitorch.datasets import dataset_from_name from bitorch.datasets.base import Augmentation from bitorch.models import model_from_name -from bitorch.datasets import dataset_from_name -from bitorch import apply_args_to_configuration from bitorch.quantizations import Quantization - from examples.pytorch_lightning.utils.log import CommandLineLogger +from utils.arg_parser import create_argparser +from utils.lightning_model import ModelWrapper +from utils.utils import configure_logging logger = logging.getLogger() -FVBITCORE_AVAILABLE = True -try: - import fvbitcore.nn as fv_nn -except ModuleNotFoundError: - logger.warning("fvbitcore not installed, will not calculate model flops!") - FVBITCORE_AVAILABLE = False - -WANDB_AVAILABLE = True -try: - from pytorch_lightning.loggers import WandbLogger - import wandb -except ModuleNotFoundError: - logger.warning("wandb not installed, will not log metrics to wandb!") - WANDB_AVAILABLE = False - - def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: """trains a model on the configured image dataset. @@ -71,7 +55,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: loggers.append(TensorBoardLogger(str(output_dir), name="tensorboard")) if args.csv_log: loggers.append(CSVLogger(str(output_dir), name="csv")) - if WANDB_AVAILABLE and args.wandb_log: + if args.wandb_log: loggers.append( WandbLogger( project=args.wandb_project, @@ -153,26 +137,23 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: persistent_workers=True, ) # type: ignore - if FVBITCORE_AVAILABLE: - data_point = torch.zeros(dataset.shape) - computational_intensity = fv_nn.FlopCountAnalysis( - model, inputs=data_point, quantization_base_class=Quantization + data_point = torch.zeros(dataset.shape) + computational_intensity = fv_nn.FlopCountAnalysis(model, inputs=data_point, quantization_base_class=Quantization) + + stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) + logger.info("\n" + table) + total_size = stats["#compressed size in bits"][""] + logger.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) + total_flops = stats["#speed up flops (app.)"][""] + logger.info("Approximated mflops: " + str(total_flops / 1e6)) + if args.wandb_log: + wandb.config.update( + { + "mflops": total_flops / 1e6, + "size in MB": total_size / 1e6 / 8.0, + } ) - stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) - logger.info("\n" + table) - total_size = stats["#compressed size in bits"][""] - logger.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) - total_flops = stats["#speed up flops (app.)"][""] - logger.info("Approximated mflops: " + str(total_flops / 1e6)) - if WANDB_AVAILABLE and args.wandb_log: - wandb.config.update( - { - "mflops": total_flops / 1e6, - "size in MB": total_size / 1e6 / 8.0, - } - ) - trainer.fit( model_wrapped, train_dataloaders=train_loader, @@ -183,7 +164,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: if __name__ == "__main__": parser, model_parser = create_argparser() - args, unparsed_model_args = parser.parse_known_args() - model_args = model_parser.parse_args(unparsed_model_args) + args_, unparsed_model_args = parser.parse_known_args() + model_args_ = model_parser.parse_args(unparsed_model_args) - main(args, model_args) + main(args_, model_args_) From da67063bc08aa2d7041c79cda57e8d7b01164652 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 10:08:46 +0200 Subject: [PATCH 058/208] update flake8 codes and add flake8-docstrings package --- bitorch/__init__.py | 8 +++-- bitorch/config.py | 12 ++++--- bitorch/datasets/imagenet.py | 1 - bitorch/layers/__init__.py | 2 +- bitorch/layers/extensions/__init__.py | 13 ++++++-- bitorch/layers/extensions/layer_container.py | 5 +-- .../layers/extensions/layer_registration.py | 1 + bitorch/layers/qconv1d.py | 9 ++++-- bitorch/layers/qembedding.py | 1 - bitorch/layers/qlinear.py | 1 - bitorch/layers/register.py | 16 +++------- bitorch/models/common_layers.py | 1 + bitorch/models/resnet_e.py | 3 +- bitorch/quantizations/base.py | 4 ++- bitorch/quantizations/dorefa.py | 1 - bitorch/quantizations/sign.py | 5 +-- bitorch/quantizations/ste_heaviside.py | 1 - examples/__init__.py | 0 examples/pytorch_lightning/utils/__init__.py | 1 + .../pytorch_lightning/utils/arg_parser.py | 5 +++ examples/pytorch_lightning/utils/log.py | 6 ++-- .../pytorch_lightning/utils/unused_args.py | 1 + requirements-dev.txt | 1 + setup.cfg | 31 ++++++++++++------- setup.py | 10 +++--- 25 files changed, 81 insertions(+), 58 deletions(-) delete mode 100644 examples/__init__.py diff --git a/bitorch/__init__.py b/bitorch/__init__.py index fdb2a26..cb6bb45 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -1,3 +1,7 @@ +""" +BITorch is a library currently under development to simplify building quantized and binary neural networks with PyTorch. +It contains implementation of the required layers, different quantization functions and examples. +""" import os from argparse import ArgumentParser, Namespace from importlib import import_module @@ -5,8 +9,8 @@ from typing import List from .config import Config -from .runtime_mode import RuntimeMode, runtime_mode_type, change_mode, pause_wrapping -from .layers import convert +from .runtime_mode import RuntimeMode, runtime_mode_type, change_mode, pause_wrapping # noqa: F401 +from .layers import convert # noqa: F401 mode: RuntimeMode = RuntimeMode.DEFAULT diff --git a/bitorch/config.py b/bitorch/config.py index 6605164..f23071d 100644 --- a/bitorch/config.py +++ b/bitorch/config.py @@ -1,12 +1,16 @@ -"""Config class for bitorch configurations. These configs can be used to specify key default values which benefit -from beeing changed easily via argparse e.g. for training scripts.""" +""" +Config class for bitorch configurations. These configs can be used to specify key default values which benefit +from beeing changed easily via argparse e.g. for training scripts. +""" from argparse import ArgumentParser, Namespace class Config: - """Config superclass that implements functionality to create argparse arguments for class attributes of - subclasses.""" + """ + Config superclass that implements functionality to create argparse arguments for class attributes of + subclasses. + """ def __init__(self) -> None: """collects all attributes of class that are not the name as configurable attributes.""" diff --git a/bitorch/datasets/imagenet.py b/bitorch/datasets/imagenet.py index f399a57..2eaa2b7 100644 --- a/bitorch/datasets/imagenet.py +++ b/bitorch/datasets/imagenet.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from torch.utils.data import Dataset diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 7e9948f..3a1a032 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -10,7 +10,7 @@ from bitorch import RuntimeMode from .debug_layers import InputGraphicalDebug, InputPrintDebug, WeightGraphicalDebug, WeightPrintDebug, ShapePrintDebug -from .extensions import CustomImplementationMixin, LayerRegistry +from .extensions import CustomImplementationMixin from .pact import Pact from .qactivation import QActivation from .qconv1d import QConv1d, QConv1d_NoAct diff --git a/bitorch/layers/extensions/__init__.py b/bitorch/layers/extensions/__init__.py index 0e0731f..0775551 100644 --- a/bitorch/layers/extensions/__init__.py +++ b/bitorch/layers/extensions/__init__.py @@ -1,9 +1,16 @@ -""" -This submodule contains objects needed to provide and manage custom layer implementations. -""" +"""This submodule contains objects needed to provide and manage custom layer implementations.""" from .layer_container import LayerContainer from .layer_implementation import DefaultImplementationMixin, CustomImplementationMixin from .layer_recipe import LayerRecipe from .layer_registration import LayerImplementation from .layer_registry import LayerRegistry + +__all__ = [ + "LayerContainer", + "DefaultImplementationMixin", + "CustomImplementationMixin", + "LayerRecipe", + "LayerImplementation", + "LayerRegistry", +] diff --git a/bitorch/layers/extensions/layer_container.py b/bitorch/layers/extensions/layer_container.py index c2c4979..b4f8a10 100644 --- a/bitorch/layers/extensions/layer_container.py +++ b/bitorch/layers/extensions/layer_container.py @@ -6,10 +6,7 @@ class LayerContainer(Generic[T]): - - """ - This class wraps another layer - but the internally contained class can be swapped out during runtime. - """ + """This class wraps another layer - but the internally contained class can be swapped out during runtime.""" internal_variable_names = [ "_layer_implementation", diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index fd2e0bc..bcc118a 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -19,6 +19,7 @@ class LayerImplementation(ABC): It registers all decorated classes in the given registry. On creation of a decorated class, it wraps the created class object in a layer container and stores the arguments used to create the layer. + It also captures which RuntimeMode(s) is/are supported by an implementation. """ registry: "LayerRegistry" diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 3566e03..9af64ec 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -15,8 +15,10 @@ class QConv1d_NoAct(Conv1d): # noqa: N801 - """Quantized 1d Convolutional Layer. Has the same api as Conv1d but lets you specify a weight quantization, that is - applied before the convolutional operation.""" + """ + Quantized 1d Convolutional Layer. Has the same api as Conv1d but lets you specify a weight quantization, that is + applied before the convolutional operation. + """ def __init__( self, @@ -26,7 +28,8 @@ def __init__( bias: bool = False, **kwargs: Any, ) -> None: - """initialization function for padding and quantization. + """ + initialization function for padding and quantization. Args: weight_quantization (Union[str, Quantization], optional): quantization module or name of quantization diff --git a/bitorch/layers/qembedding.py b/bitorch/layers/qembedding.py index abaeee6..823b560 100644 --- a/bitorch/layers/qembedding.py +++ b/bitorch/layers/qembedding.py @@ -47,7 +47,6 @@ def forward( Returns: Tensor: embeddings for given sequences """ - # necessary for torch 1.8 compliance if hasattr(self, "padding_idx"): embeddings = embedding_bag( diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index f6c594f..40e4ba5 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -75,7 +75,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensors: forwarded tensor """ - return linear(self.activation(x), self.weight_quantization(self.weight), self.bias) diff --git a/bitorch/layers/register.py b/bitorch/layers/register.py index 9577add..904f412 100644 --- a/bitorch/layers/register.py +++ b/bitorch/layers/register.py @@ -42,9 +42,7 @@ def convert_layers_to( class QLinearImplementation(LayerImplementation): - """ - Decorator for :class:`QLinear` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ + """Decorator for :class:`QLinear` implementations.""" def __init__(self, supports_modes: runtime_mode_type) -> None: """ @@ -55,9 +53,7 @@ def __init__(self, supports_modes: runtime_mode_type) -> None: class QConv1dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv1d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ + """Decorator for :class:`QConv1d` implementations.""" def __init__(self, supports_modes: runtime_mode_type) -> None: """ @@ -68,9 +64,7 @@ def __init__(self, supports_modes: runtime_mode_type) -> None: class QConv2dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv2d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ + """Decorator for :class:`QConv2d` implementations.""" def __init__(self, supports_modes: runtime_mode_type) -> None: """ @@ -81,9 +75,7 @@ def __init__(self, supports_modes: runtime_mode_type) -> None: class QConv3dImplementation(LayerImplementation): - """ - Decorator for :class:`QConv3d` implementations, captures which RuntimeMode(s) is/are supported by an implementation. - """ + """Decorator for :class:`QConv3d` implementations.""" def __init__(self, supports_modes: runtime_mode_type) -> None: """ diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index 16abe78..a4c938e 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -3,6 +3,7 @@ def get_initial_layers(variant: str, input_channels: int, output_channels: int) -> List[nn.Module]: + """Get commonly used layers to extract initial features from the image.""" layers: List[nn.Module] = [] if variant == "imagenet": layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False)) diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 4b7c4a9..299acbb 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -108,8 +108,7 @@ def make_layer(self, layers: int, in_channels: int, out_channels: int, stride: i Returns: nn.Sequential: the model containing the building blocks """ - - # this tricks adds shortcut connections between original resnet blocks + # this trick adds shortcut connections between original resnet blocks # we multiple number of blocks by 2, but add only one layer instead of two in each block layers = layers * 2 diff --git a/bitorch/quantizations/base.py b/bitorch/quantizations/base.py index cf8f5db..41e64ed 100644 --- a/bitorch/quantizations/base.py +++ b/bitorch/quantizations/base.py @@ -55,7 +55,9 @@ def bitwidth(self) -> int: return self.bit_width def quantize(self, x: torch.Tensor) -> torch.Tensor: - """quantize the input tensor. It is recommended to use a torch.Function to also maniputlate backward behavior. + """Quantize the input tensor. + + It is recommended to use a torch.Function to also manipulate backwards behavior. See the implementations of sign or dorefa quantization functions for more examples. Args: diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index 86191ce..adb1ffd 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -107,5 +107,4 @@ def quantize(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: DoReFaed tensor x """ - return InputDoReFaFunction.apply(x, self.bit_width) diff --git a/bitorch/quantizations/sign.py b/bitorch/quantizations/sign.py index 34ed4c5..2cdb88d 100644 --- a/bitorch/quantizations/sign.py +++ b/bitorch/quantizations/sign.py @@ -1,8 +1,9 @@ """Sign Function Implementation""" -from typing import Tuple, Union, Optional -import torch import typing + +import torch + from .base import Quantization, STE diff --git a/bitorch/quantizations/ste_heaviside.py b/bitorch/quantizations/ste_heaviside.py index e91651f..bf9195c 100644 --- a/bitorch/quantizations/ste_heaviside.py +++ b/bitorch/quantizations/ste_heaviside.py @@ -20,7 +20,6 @@ def forward( Returns: torch.Tensor: the quantized input tensor """ - quantized_tensor = torch.where( input_tensor > 0, torch.tensor(1.0, device=input_tensor.device), diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/pytorch_lightning/utils/__init__.py b/examples/pytorch_lightning/utils/__init__.py index e69de29..f85768d 100644 --- a/examples/pytorch_lightning/utils/__init__.py +++ b/examples/pytorch_lightning/utils/__init__.py @@ -0,0 +1 @@ +"""Utilities for image classification example.""" diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index e42daa6..46aa837 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -80,6 +80,11 @@ def add_logging_args(parser: ArgumentParser) -> None: def add_checkpoint_args(parser: ArgumentParser) -> None: + """adds cli parameters for checkpoint logging + + Args: + parser (ArgumentParser): the main argument parser + """ checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") checkpoint.add_argument( "--checkpoint-dir", diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index b425775..ea49921 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -17,7 +17,7 @@ ) -def display_time(seconds: float, granularity: int = 2) -> str: +def _display_time(seconds: float, granularity: int = 2) -> str: result: List[str] = [] seconds = int(round(seconds)) @@ -99,13 +99,13 @@ def on_train_batch_end( time_in_this_epoch = time.time() - self._epoch_start_time epoch_total_est = int(round((time_in_this_epoch * self.total_train_batches) / self.train_batch_idx)) - eta_epoch = display_time(epoch_total_est - time_in_this_epoch) + eta_epoch = _display_time(epoch_total_est - time_in_this_epoch) full_epochs_left = trainer.max_epochs - trainer.current_epoch if full_epochs_left < 0: full_epochs_left = 0 if self._average_epoch_time() > 0: epoch_total_est = self._average_epoch_time() + self._average_validation_time() - eta_train = display_time(epoch_total_est - time_in_this_epoch + full_epochs_left * epoch_total_est) + eta_train = _display_time(epoch_total_est - time_in_this_epoch + full_epochs_left * epoch_total_est) epoch_info = f"Epoch {trainer.current_epoch:3d}" batch_info = f"{self.train_batch_idx:4d}/{self.total_train_batches:4d} ({percent:5.1f}%)" diff --git a/examples/pytorch_lightning/utils/unused_args.py b/examples/pytorch_lightning/utils/unused_args.py index e92455e..7adeb79 100644 --- a/examples/pytorch_lightning/utils/unused_args.py +++ b/examples/pytorch_lightning/utils/unused_args.py @@ -4,6 +4,7 @@ def clean_hyperparameters(args: Namespace) -> Namespace: + """Remove args which are not passed to the constructor in our training script.""" clean_args = Namespace() for key in args.__dict__.keys(): if key in UNUSED_PL_ARGS: diff --git a/requirements-dev.txt b/requirements-dev.txt index ebd80fb..4ce8735 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ black build flake8 +flake8-docstrings mypy~=0.920 myst-nb nbclient==0.5.13 diff --git a/setup.cfg b/setup.cfg index c552139..91045fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,26 +13,35 @@ exclude = .git, .venv, venv, - dist - tests -select = C,E,F,W,B,B950 + dist, + tests, + examples/mnist +select = C,D,E,F,W,B,B950 ignore = + D100, + # D100: Missing docstring in public module + D101, + # D101: Missing docstring in public class + D102, + # D102: Missing docstring in public method + D105, + # D105: Missing docstring in magic method D107, + # D107: Missing docstring in __init__ D204, + # D204: 1 blank line required after class docstring D205, + # D205: 1 blank line required between summary line and description D400, + # D400: First line should end with a period D401, + # D401: First line should be in imperative mood D403, + # D403: First word of the first line should be properly capitalized DAR103, - E203, - E402, - E402, + # DAR103: The docstring parameter type doesn't match function. E501, - F401, - F403, - F821, - W503, - W504, + # E501: Line too long (82 > 79 characters) [pydocstyle] select = D417 # Missing argument descriptions in the docstring diff --git a/setup.py b/setup.py index 9662b0d..451cb92 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ print("version:", version) -def get_requirements(file_path: Union[Path, str]): +def _get_requirements(file_path: Union[Path, str]): return [requirement.strip() for requirement in (root_path / file_path).open().readlines()] @@ -32,11 +32,11 @@ def get_requirements(file_path: Union[Path, str]): long_description=readme_content, long_description_content_type="text/markdown", packages=setuptools.find_packages(exclude="tests"), - install_requires=get_requirements("requirements.txt"), + install_requires=_get_requirements("requirements.txt"), extras_require={ - "dev": get_requirements("requirements-dev.txt"), - "opt": get_requirements("examples/pytorch_lightning/requirements.txt") - + get_requirements("examples/mnist/requirements.txt"), + "dev": _get_requirements("requirements-dev.txt"), + "opt": _get_requirements("examples/pytorch_lightning/requirements.txt") + + _get_requirements("examples/mnist/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", From 4049a9fd0ecc21fb38d865301216647092a4ceac Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 14:19:25 +0200 Subject: [PATCH 059/208] adapt changelog, remove mnist requirements until inference engine is published --- CHANGELOG.md | 13 +++++++------ setup.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6147b11..c8e1836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Simple example script for MNIST -- Support for integration of bitorch's inference engine for the following layers +- simple example script for MNIST +- support for integration of bitorch's inference engine for the following layers - QLinear - -### Added - -- code formatting using black + - QConv ### Changed +- requirements changed: + - requirements for examples are now stored at their respective folders + - optional requirements now install everything needed to run all examples +- code is now formatted with the black code formatter - using PyTorch's implementation of RAdam - renamed the `bitwidth` attribute of quantization functions to `bit_width` diff --git a/setup.py b/setup.py index 451cb92..635b347 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def _get_requirements(file_path: Union[Path, str]): extras_require={ "dev": _get_requirements("requirements-dev.txt"), "opt": _get_requirements("examples/pytorch_lightning/requirements.txt") - + _get_requirements("examples/mnist/requirements.txt"), + # + _get_requirements("examples/mnist/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", From b4f2eabafb656dee8eabe42950e7b720db6aa8ab Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 14:21:00 +0200 Subject: [PATCH 060/208] increase dev version --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 3f0adea..3064722 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.0-dev.2 +0.2.0.dev3 From a686cd7c7674cbb7861bf7a0e816d34833ea2b74 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 23 Jun 2022 15:30:01 +0200 Subject: [PATCH 061/208] correct version for dev release and update files in package --- CHANGELOG.md | 2 +- README.md | 2 +- examples/__init__.py | 1 + examples/mnist/__init__.py | 4 ++++ examples/pytorch_lightning/__init__.py | 1 + setup.cfg | 2 ++ setup.py | 28 +++++++++++++++++--------- version.txt | 2 +- 8 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/mnist/__init__.py create mode 100644 examples/pytorch_lightning/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e1836..65c41da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.3.x] - Unreleased +## [0.3.0] - Unreleased ### Added diff --git a/README.md b/README.md index ddfd468..45fc432 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Our current roadmap contains: - Extending the model zoo with pre-trained models of state-of-the-art approaches - Adding examples for advanced training methods with multiple stages, knowledge distillation, etc. -All changes are tracked in the [changelog](CHANGELOG.md). +All changes are tracked in the [changelog](https://github.com/hpi-xnor/bitorch/blob/main/CHANGELOG.md). ## Installation diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..0868335 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +"""This package contains examples for BITorch.""" diff --git a/examples/mnist/__init__.py b/examples/mnist/__init__.py new file mode 100644 index 0000000..1ce417a --- /dev/null +++ b/examples/mnist/__init__.py @@ -0,0 +1,4 @@ +""" +This package contains an example for training an image classification model on the MNIST data set with BITorch +and deploying it with the inference engine. +""" diff --git a/examples/pytorch_lightning/__init__.py b/examples/pytorch_lightning/__init__.py new file mode 100644 index 0000000..aa5b8b1 --- /dev/null +++ b/examples/pytorch_lightning/__init__.py @@ -0,0 +1 @@ +"""This package contains an example for image classification with BITorch.""" diff --git a/setup.cfg b/setup.cfg index 91045fd..1bf55c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,8 @@ ignore = # DAR103: The docstring parameter type doesn't match function. E501, # E501: Line too long (82 > 79 characters) + W503, + # W503: line break before binary operator [pydocstyle] select = D417 # Missing argument descriptions in the docstring diff --git a/setup.py b/setup.py index 635b347..7ca3b69 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Union +from typing import Union, List import setuptools @@ -15,13 +15,21 @@ print("version:", version) -def _get_requirements(file_path: Union[Path, str]): - return [requirement.strip() for requirement in (root_path / file_path).open().readlines()] +def _get_requirements(*file_path: Union[Path, str]): + result = [] + for fp in file_path: + result.extend(list(requirement.strip() for requirement in (root_path / fp).open().readlines())) + return result + + +def _get_files_recursively(glob: str, root: str = ".") -> List[str]: + return list(str(x) for x in Path(root).rglob(glob)) with open("README.md", "r", encoding="utf-8") as handle: readme_content = handle.read() + setuptools.setup( name="bitorch", url="https://github.com/hpi-xnor/bitorch", @@ -31,12 +39,11 @@ def _get_requirements(file_path: Union[Path, str]): description="A package for building and training quantized and binary neural networks with Pytorch", long_description=readme_content, long_description_content_type="text/markdown", - packages=setuptools.find_packages(exclude="tests"), + packages=setuptools.find_packages(), install_requires=_get_requirements("requirements.txt"), extras_require={ "dev": _get_requirements("requirements-dev.txt"), - "opt": _get_requirements("examples/pytorch_lightning/requirements.txt") - # + _get_requirements("examples/mnist/requirements.txt"), + "opt": _get_requirements(*_get_files_recursively("requirements*.txt", root="examples")), }, classifiers=[ "Development Status :: 3 - Alpha", @@ -52,10 +59,13 @@ def _get_requirements(file_path: Union[Path, str]): ( ".", [ + "AUTHORS", + "CHANGELOG.md", + "mypy.ini", "version.txt", - "requirements.txt", - "requirements-dev.txt", - ], + ] + + _get_files_recursively("requirements*.txt") + + _get_files_recursively("README.md", root="examples"), ), ], ) diff --git a/version.txt b/version.txt index 3064722..13668bb 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.0.dev3 +0.3.0.dev0 From 5c8ebc10f99f4d7d30356fc8245c920aa3173df8 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 24 Jun 2022 10:07:32 +0200 Subject: [PATCH 062/208] mypy fix --- examples/pytorch_lightning/image_classification.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 06d7a6a..4ee37b5 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -13,7 +13,7 @@ import argparse import logging from pathlib import Path -from typing import List +from typing import List, Any import fvbitcore.nn as fv_nn import torch @@ -64,7 +64,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: save_dir=str(output_dir), ) # type: ignore ) - callbacks = [] + callbacks: List[Any] = [] if args.checkpoint_dir is not None: callbacks.append( ModelCheckpoint( @@ -82,7 +82,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: if len(loggers) > 0: lr_monitor = LearningRateMonitor(logging_interval="step") - callbacks.append(lr_monitor) # type: ignore + callbacks.append(lr_monitor) dataset = dataset_from_name(args.dataset) From 46820cee73aef1fd1e34548131bbd979c0a8cfaa Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 24 Jun 2022 10:31:27 +0200 Subject: [PATCH 063/208] scheduled pipeline checks compatibility with newer python versions --- .gitlab-ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 610a4cf..82d2e6a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,6 +17,10 @@ before_script: - mypy --config-file mypy.ini - black . --check --verbose --diff --color +.scheduled-only: + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + codestyle: extends: .codestyle @@ -24,6 +28,18 @@ codestyle:3.8: extends: .codestyle image: python:3.8 +codestyle:3.9: + extends: + - .codestyle + - .scheduled-only + image: python:3.9 + +codestyle:3.10: + extends: + - .codestyle + - .scheduled-only + image: python:3.10 + .test: stage: test script: @@ -37,6 +53,18 @@ test:3.8: extends: .test image: python:3.8 +test:3.9: + extends: + - .test + - .scheduled-only + image: python:3.9 + +test:3.10: + extends: + - .test + - .scheduled-only + image: python:3.10 + test-build-doc: stage: test script: From a552823fb7d6d81d983ce9bafaaeb7de084da4e7 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 24 Jun 2022 11:02:24 +0200 Subject: [PATCH 064/208] make more pipelines only run when scheduled --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 82d2e6a..50f7fff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,9 @@ codestyle: extends: .codestyle codestyle:3.8: - extends: .codestyle + extends: + - .codestyle + - .scheduled-only image: python:3.8 codestyle:3.9: @@ -50,7 +52,9 @@ test: extends: .test test:3.8: - extends: .test + extends: + - .test + - .scheduled-only image: python:3.8 test:3.9: From e9e63164c1188d930a8e2069a29ac304ef922cf3 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 28 Jun 2022 08:53:55 +0200 Subject: [PATCH 065/208] make examples work as standalone by installing bitorch, update CI --- .gitlab-ci.yml | 32 ++++++--------------- examples/mnist/requirements.txt | 1 + examples/pytorch_lightning/requirements.txt | 5 ++-- requirements-dev.txt | 3 +- setup.py | 9 ++++-- 5 files changed, 20 insertions(+), 30 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 50f7fff..a8740e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,18 +24,6 @@ before_script: codestyle: extends: .codestyle -codestyle:3.8: - extends: - - .codestyle - - .scheduled-only - image: python:3.8 - -codestyle:3.9: - extends: - - .codestyle - - .scheduled-only - image: python:3.9 - codestyle:3.10: extends: - .codestyle @@ -51,18 +39,6 @@ codestyle:3.10: test: extends: .test -test:3.8: - extends: - - .test - - .scheduled-only - image: python:3.8 - -test:3.9: - extends: - - .test - - .scheduled-only - image: python:3.9 - test:3.10: extends: - .test @@ -74,3 +50,11 @@ test-build-doc: script: - apt-get update && apt-get install -y pandoc - sphinx-build -b html docs/source/ docs/build/ -a + +test-doc-completeness: + extends: .scheduled-only + stage: test + script: + - flake8 --version + # explicitly select Docstring errors and ignore to overwrite config in setup.cfg + - flake8 --select=D1 --ignore=E501 diff --git a/examples/mnist/requirements.txt b/examples/mnist/requirements.txt index 99f86ab..96c05b9 100644 --- a/examples/mnist/requirements.txt +++ b/examples/mnist/requirements.txt @@ -1 +1,2 @@ +bitorch bitorch_inference_engine diff --git a/examples/pytorch_lightning/requirements.txt b/examples/pytorch_lightning/requirements.txt index 38e59b2..7308aef 100644 --- a/examples/pytorch_lightning/requirements.txt +++ b/examples/pytorch_lightning/requirements.txt @@ -1,5 +1,6 @@ -wandb +bitorch fvbitcore -sklearn pytorch_lightning +sklearn torchmetrics +wandb diff --git a/requirements-dev.txt b/requirements-dev.txt index 4ce8735..7f54ff4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,8 @@ nbclient==0.5.13 nbsphinx-link==1.3.0 nbsphinx==0.8.8 pep8-naming +pre-commit pytest +pytest-cov sphinx twine -pre-commit diff --git a/setup.py b/setup.py index 7ca3b69..e82c4ef 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,13 @@ def _get_requirements(*file_path: Union[Path, str]): - result = [] + requirements_list = [] for fp in file_path: - result.extend(list(requirement.strip() for requirement in (root_path / fp).open().readlines())) - return result + requirements_list.extend(list(requirement.strip() for requirement in (root_path / fp).open().readlines())) + # exclude bitorch from examples + if "bitorch" in requirements_list: + requirements_list.remove("bitorch") + return requirements_list def _get_files_recursively(glob: str, root: str = ".") -> List[str]: From 317ff8c2a47a6f9b22a0e9daed5c39a1aceea332 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 28 Jun 2022 09:44:02 +0200 Subject: [PATCH 066/208] rename test classes so as to not be confused for actual tests, add coverage --- .gitlab-ci.yml | 30 +++++++++++++++++++-------- setup.cfg | 10 +++++++++ tests/layers/test_switchable_layer.py | 4 ++-- tests/models/test_model_conversion.py | 4 ++-- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8740e9..f145b76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,10 +2,13 @@ image: python:3.7 before_script: - python --version - - pip install -e .[dev] --extra-index-url https://download.pytorch.org/whl/cu113 - - pwd - - ls -l - - python -c "import sys;print(sys.path)" + - pip install -e ".[dev]" --extra-index-url https://download.pytorch.org/whl/cu113 + +.scheduled-only: + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + +# Checking codestyle .codestyle: stage: test @@ -17,10 +20,6 @@ before_script: - mypy --config-file mypy.ini - black . --check --verbose --diff --color -.scheduled-only: - rules: - - if: $CI_PIPELINE_SOURCE == "schedule" - codestyle: extends: .codestyle @@ -30,14 +29,25 @@ codestyle:3.10: - .scheduled-only image: python:3.10 +# Running tests + .test: stage: test script: - pytest --version - python -m pytest . -test: +test-and-coverage: extends: .test + script: + - pytest --version + - python -m pytest --cov bitorch --cov-report xml . + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml test:3.10: extends: @@ -45,6 +55,8 @@ test:3.10: - .scheduled-only image: python:3.10 +# Documentation + test-build-doc: stage: test script: diff --git a/setup.cfg b/setup.cfg index 1bf55c6..cf456c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,3 +47,13 @@ ignore = [pydocstyle] select = D417 # Missing argument descriptions in the docstring + +[coverage:run] +branch = True + +[coverage:report] +show_missing = True +skip_covered = True + +[coverage:xml] +output = coverage.xml diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index 2d27275..5ccc966 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -26,7 +26,7 @@ def self_function(self): return self -class TestLayerContainer(LayerContainer): +class _LayerContainer(LayerContainer): patch = LayerContainer.patch + [ "self_function", ] @@ -35,7 +35,7 @@ class TestLayerContainer(LayerContainer): @pytest.mark.parametrize("test_wrapped_layer", [False, True]) def test_switchable_layer(test_wrapped_layer): if test_wrapped_layer: - layer = TestLayerContainer(Layer, 42) + layer = _LayerContainer(Layer, 42) else: layer = Layer(42) assert layer.x == 42 diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 66a1ddc..98ae191 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -27,7 +27,7 @@ TEST_MODE = RuntimeMode.INFERENCE_AUTO -class TestModel(Model): +class _TestModel(Model): def __init__(self): super().__init__(dataset=MNIST) self.q_conv2d = QConv2d(1, 32, 3, 1, 1) @@ -148,7 +148,7 @@ def create_clone_from(cls, recipe: LayerRecipe, device: torch.device) -> Any: def _test(): x = torch.rand(1, 1, 28, 28) - net = TestModel() + net = _TestModel() assert not hasattr(net.q_linear, "is_test_implementation") assert not hasattr(net.q_conv2d, "is_test_implementation") From cafbee04283d6e7d199925143587036c6167d943 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 28 Jun 2022 14:35:31 +0200 Subject: [PATCH 067/208] use explicit coverage commands --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f145b76..8025438 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,9 @@ test-and-coverage: extends: .test script: - pytest --version - - python -m pytest --cov bitorch --cov-report xml . + - coverage run -m pytest + - coverage report + - coverage xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: reports: From d39b2d66404641553ab41495e781453ec36e5ea1 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 30 Jun 2022 14:01:19 +0200 Subject: [PATCH 068/208] increase coverage precision --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index cf456c7..09fb933 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,7 @@ branch = True [coverage:report] show_missing = True skip_covered = True +precision = 2 [coverage:xml] output = coverage.xml From 0ea4a7369267fa7ce7b14cd86f086f704f0929f3 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 30 Jun 2022 13:51:29 +0200 Subject: [PATCH 069/208] make it possible to retrieve layers from qconv as well --- bitorch/layers/__init__.py | 9 ++-- bitorch/layers/qconv1d.py | 3 +- bitorch/layers/qconv2d.py | 39 ++------------ bitorch/layers/qconv3d.py | 3 +- bitorch/layers/qconv_mixin.py | 31 +++++++++++ tests/layers/test_layer_arg_retrieval.py | 66 ++++++++++++++++++++++++ tests/layers/test_qconv.py | 27 ++-------- tests/layers/test_qlinear.py | 38 +------------- 8 files changed, 115 insertions(+), 101 deletions(-) create mode 100644 bitorch/layers/qconv_mixin.py create mode 100644 tests/layers/test_layer_arg_retrieval.py diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 3a1a032..e5e975d 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -13,9 +13,9 @@ from .extensions import CustomImplementationMixin from .pact import Pact from .qactivation import QActivation -from .qconv1d import QConv1d, QConv1d_NoAct -from .qconv2d import QConv2d, QConv2d_NoAct -from .qconv3d import QConv3d, QConv3d_NoAct +from .qconv1d import QConv1d, QConv1dBase, QConv1d_NoAct +from .qconv2d import QConv2d, QConv2dBase, QConv2d_NoAct +from .qconv3d import QConv3d, QConv3dBase, QConv3d_NoAct from .qembedding import QEmbedding, QEmbeddingBag from .qlinear import QLinear, QLinearBase from .register import all_layer_registries @@ -30,6 +30,9 @@ "QConv1d", "QConv2d", "QConv3d", + "QConv1dBase", + "QConv2dBase", + "QConv3dBase", "QConv1d_NoAct", "QConv2d_NoAct", "QConv3d_NoAct", diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 9af64ec..856f8f5 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -11,6 +11,7 @@ from .config import config from .extensions import DefaultImplementationMixin from .qactivation import QActivation +from .qconv_mixin import QConvArgsProviderMixin from .register import QConv1dImplementation @@ -77,7 +78,7 @@ def forward(self, input: Tensor) -> Tensor: ) -class QConv1dBase(QConv1d_NoAct): # type: ignore +class QConv1dBase(QConvArgsProviderMixin, QConv1d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index c8a2d8f..908030d 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -1,6 +1,6 @@ """Module containing the quantized 2d convolution layer""" -from typing import Union, Any, Dict +from typing import Union, Any from torch import Tensor from torch.nn import Conv2d, init @@ -9,8 +9,9 @@ from bitorch import RuntimeMode from bitorch.quantizations import Quantization from .config import config -from .extensions import DefaultImplementationMixin, LayerRecipe +from .extensions import DefaultImplementationMixin from .qactivation import QActivation +from .qconv_mixin import QConvArgsProviderMixin from .register import QConv2dImplementation @@ -71,7 +72,7 @@ def forward(self, input: Tensor) -> Tensor: ) -class QConv2dBase(QConv2d_NoAct): # type: ignore +class QConv2dBase(QConvArgsProviderMixin, QConv2d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, @@ -93,38 +94,6 @@ def __init__( super().__init__(*args, weight_quantization=weight_quantization, **kwargs) self.activation = QActivation(input_quantization, gradient_cancellation_threshold) - @staticmethod - def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: - """ - Gather all arguments that were used to create a QLinear layer with argument names. - Can be used to recreate a layer with identical arguments. - - Returns: - A dictionary with all arguments (key is the argument name as a string even for positional arguments) - """ - _ = ... - return { - # "in_features": recipe.get_positional_arg(0), - # "out_features": recipe.get_positional_arg(1), - # "input_quantization": recipe.layer.input_quantization, - # "gradient_cancellation_threshold": recipe.layer.gradient_cancellation_threshold, - # "weight_quantization": recipe.layer.weight_quantization, - # "bias": recipe.get_arg(5, "bias", True), - "in_channels": _, - "out_channels": _, - "kernel_size": _, - "stride": _, - "padding": _, - "dilation": _, - "transposed": _, - "output_padding": _, - "groups": _, - "bias": _, - "padding_mode": _, - "device": recipe.get_arg(6, "device", None), - "dtype": recipe.get_arg(7, "dtype", None), - } - def forward(self, input_tensor: Tensor) -> Tensor: """forward the input tensor through the activation and quantized convolution layer. diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 665d0d7..4bcb13f 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -11,6 +11,7 @@ from .config import config from .extensions import DefaultImplementationMixin from .qactivation import QActivation +from .qconv_mixin import QConvArgsProviderMixin from .register import QConv3dImplementation @@ -71,7 +72,7 @@ def forward(self, input: Tensor) -> Tensor: ) -class QConv3dBase(QConv3d_NoAct): # type: ignore +class QConv3dBase(QConvArgsProviderMixin, QConv3d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, diff --git a/bitorch/layers/qconv_mixin.py b/bitorch/layers/qconv_mixin.py new file mode 100644 index 0000000..66c82fe --- /dev/null +++ b/bitorch/layers/qconv_mixin.py @@ -0,0 +1,31 @@ +from typing import Dict, Any + +from .extensions import LayerRecipe + + +class QConvArgsProviderMixin: + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + @staticmethod + def get_args_as_kwargs(recipe: LayerRecipe) -> Dict[str, Any]: + """ + Gather all arguments that were used to create a QLinear layer with argument names. + Can be used to recreate a layer with identical arguments. + + Returns: + A dictionary with all arguments (key is the argument name as a string even for positional arguments) + """ + return { + "in_channels": recipe.get_positional_arg(0), + "out_channels": recipe.get_positional_arg(1), + "kernel_size": recipe.get_positional_arg(2), + "stride": recipe.get_arg(3, "stride", None), + "padding": recipe.get_arg(4, "padding", None), + "dilation": recipe.get_arg(5, "dilation", None), + "groups": recipe.get_arg(6, "groups", None), + "bias": recipe.get_arg(7, "bias", True), + "padding_mode": recipe.get_arg(8, "padding_mode", None), + "device": recipe.get_arg(9, "device", None), + "dtype": recipe.get_arg(10, "dtype", None), + } diff --git a/tests/layers/test_layer_arg_retrieval.py b/tests/layers/test_layer_arg_retrieval.py new file mode 100644 index 0000000..1a68e60 --- /dev/null +++ b/tests/layers/test_layer_arg_retrieval.py @@ -0,0 +1,66 @@ +import pytest + +from bitorch.layers import ( + QConv1d, + QConv1dBase, + QConv2d, + QConv2dBase, + QConv3d, + QConv3dBase, + QLinear, + QLinearBase, +) +from bitorch.quantizations import Sign + + +Q_CONV_ARGS = [ + ("in_channels", 16), + ("out_channels", 64), + ("kernel_size", 3), + ("stride", 1), + ("padding", 1), + ("dilation", 1), + ("groups", 1), + ("bias", False), + ("padding_mode", "zeros"), + ("device", None), + ("dtype", None), +] +Q_LINEAR_ARGS = [ + ("in_features", 64), + ("out_features", 32), + ("input_quantization", Sign()), + ("gradient_cancellation_threshold", 1.3), + ("weight_quantization", Sign()), + ("bias", False), + ("device", None), + ("dtype", None), +] + + +@pytest.mark.parametrize( + "all_args, layer, base_layer, num_positional_args", + [ + [Q_CONV_ARGS, QConv1d, QConv1dBase, 3], + [Q_CONV_ARGS, QConv2d, QConv2dBase, 3], + [Q_CONV_ARGS, QConv3d, QConv3dBase, 3], + [Q_LINEAR_ARGS, QLinear, QLinearBase, 2], + ], +) +def test_args_function(all_args, layer, base_layer, num_positional_args: int): + expected_result = {} + layer_args = [] + layer_kwargs = {} + + for j, (key, val) in enumerate(all_args): + expected_result[key] = val + if j < num_positional_args: + layer_args.append(val) + else: + layer_kwargs[key] = val + + layer = layer(*layer_args, **layer_kwargs) + result = base_layer.get_args_as_kwargs(layer.recipe) + assert result.keys() == expected_result.keys() + for k in expected_result.keys(): + assert expected_result[k] == result[k] diff --git a/tests/layers/test_qconv.py b/tests/layers/test_qconv.py index ecee778..4e5617c 100644 --- a/tests/layers/test_qconv.py +++ b/tests/layers/test_qconv.py @@ -66,11 +66,12 @@ "padding": 1, }, ), -] * 10 +] +@pytest.mark.parametrize("execution_number", range(10)) @pytest.mark.parametrize("conv_layer, conv_fn, input_shape, args, kwargs", TEST_INPUT_DATA) -def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): +def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs, execution_number): input_values = np.random.uniform(-1, 1, input_shape) layer = conv_layer(*args, **kwargs) input_tensor = torch.tensor(input_values).float().requires_grad_(True) @@ -103,25 +104,3 @@ def test_qconv(conv_layer, conv_fn, input_shape, args, kwargs): assert torch.equal(padded_input, layer._apply_padding(expected_tensor)) assert torch.equal(result1, direct_result) assert torch.equal(grad1, grad2) - - -@pytest.mark.parametrize( - "all_args", - [ - [ - ("in_channels", 16), - ("out_channels", 64), - ("kernel_size", 3), - ("stride", 1), - ("padding", 1), - ("dilation", 1), - ("groups", 1), - ("bias", True), - ("padding_mode", "zeros"), - ("device", None), - ("dtype", None), - ], - ], -) -def test_args_function(all_args): - pass diff --git a/tests/layers/test_qlinear.py b/tests/layers/test_qlinear.py index 1fb2172..04e8ae3 100644 --- a/tests/layers/test_qlinear.py +++ b/tests/layers/test_qlinear.py @@ -3,7 +3,7 @@ from torch.nn import Parameter from bitorch.layers.qactivation import QActivation -from bitorch.layers.qlinear import QLinear, QLinearBase +from bitorch.layers.qlinear import QLinear from bitorch.quantizations import Sign, quantization_from_name TEST_INPUT_DATA = [[0.0, 0.0], [1.0, 0.0], [-1.0, 1.0], [0.3, -0.3], [1e12, -1e12]] @@ -28,39 +28,3 @@ def test_qlinear(input_values, quantization): y = layer(x) assert torch.equal(result, y) - - -@pytest.mark.parametrize( - "all_args", - [ - [ - ("in_features", 64), - ("out_features", 32), - ("input_quantization", Sign()), - ("gradient_cancellation_threshold", 1.3), - ("weight_quantization", Sign()), - ("bias", False), - ("device", None), - ("dtype", None), - ], - ], -) -def test_args_function(all_args): - positional_args = 2 - - expected_result = {} - layer_args = [] - layer_kwargs = {} - - for j, (key, val) in enumerate(all_args): - expected_result[key] = val - if j < positional_args: - layer_args.append(val) - else: - layer_kwargs[key] = val - - layer = QLinear(*layer_args, **layer_kwargs) - result = QLinearBase.get_args_as_kwargs(layer.recipe) - assert result.keys() == expected_result.keys() - for k in expected_result.keys(): - assert expected_result[k] == result[k] From ca4d19c8d9bce8f6d286f49b464e7993fb8d285b Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 1 Jul 2022 10:47:09 +0200 Subject: [PATCH 070/208] added dlrm model, not working yet --- bitorch/models/dlrm.py | 434 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 bitorch/models/dlrm.py diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py new file mode 100644 index 0000000..6a26728 --- /dev/null +++ b/bitorch/models/dlrm.py @@ -0,0 +1,434 @@ +from argparse import ArgumentParser +import math +from pytorch_lightning import LightningModule +from enum import Enum +from typing import List, Optional, Tuple, Union +import logging +import torch +from torch.nn import ( + Linear, + Sigmoid, + EmbeddingBag, + ModuleList, + ParameterList, + Parameter, + BatchNorm1d, +) +import torch.nn.functional as F +import numpy as np +import sklearn.metrics as metrics +from bitorch.layers import QLinear + +# from .utils import create_loss_function, create_optimizer, create_activation_function, parse_layer_sizes, str2bool +from bitorch.layers.qembedding import QEmbeddingBag + +class Weighted_Pooling_Type(Enum): + NONE = 0 + FIXED = 1 + LEARNED = 2 + +class Interaction_Operation_Type(Enum): + PRODUCT = "product" + CONCAT = "concat" + SUM = "sum" + +class MLP(torch.nn.Module): + """Mlp class for DLRM mlps. this mainly is used to properly pass shortcut data""" + name = "MLP" + def __init__(self, layers: ModuleList, name: str = "MLP", shortcut_out_index: Union[int, None] = None, shortcut_in_index: Union[int, None] = None): + super().__init__() + self.shortcut_out_index = shortcut_out_index + self.shortcut_in_index = shortcut_in_index + self.layers = layers + self.name = name + logging.info(f"shortcut_out_index {shortcut_out_index}") + logging.info(f"shortcut_in_index {shortcut_in_index}") + + def forward(self, X, shortcut_value = None): + out = None + Y = X + for index, layer in enumerate(self.layers): + if index == self.shortcut_in_index: + Y = Y + shortcut_value + Y = layer(Y) + if index == self.shortcut_out_index: + out = Y + return Y, out + + def __str__(self) -> str: + out = f"{self.name}\n" + out += "="*10 + out += "\n".join(self.layers) + return out + +def create_mlp( + layer_sizes: List[int], + quantized: bool = False) -> MLP: + """creates a mlp module + + Args: + layer_sizes (List[int]): linear layer unit sizes + sigmoid_layer_idx (int): the layer number to use a sigmoid activation function. + all other layers will have relu activation. + """ + input_size = layer_sizes[0] + this_shortcut_out_index = None + this_shortcut_in_index = None + mlp_layers = [] if not batch_norm else [BatchNorm1d(input_size)] + if full_precision_layers is None: + full_precision_layers = [False] * len(layer_sizes) + + for idx, layer_size in enumerate(layer_sizes[1:]): + output_size = layer_size + if batch_norm_before_sign: + mlp_layers.append(BatchNorm1d(input_size)) + mlp_layers.append( + Linear(input_size, output_size, bias=True) if not quantized or full_precision_layers[idx + 1] else + QLinear(input_size, output_size, bias=False) + ) + + if idx == shortcut_out_index and this_shortcut_out_index is None: + this_shortcut_out_index = len(mlp_layers) + if idx == shortcut_in_index and this_shortcut_in_index is None: + this_shortcut_in_index = len(mlp_layers) + mean = 0.0 # std_dev = np.sqrt(variance) + std_dev = np.sqrt(2 / (output_size + input_size)) # np.sqrt(1 / m) # np.sqrt(1 / n) + mlp_weight = np.random.normal(mean, std_dev, size=(output_size, input_size)).astype(np.float32) + std_dev = np.sqrt(1 / output_size) # np.sqrt(2 / (m + 1)) + mlp_bias = np.random.normal(mean, std_dev, size=output_size).astype(np.float32) + # approach 1 + mlp_layers[-1].weight.data = torch.tensor(mlp_weight, requires_grad=True) + if mlp_layers[-1].bias is not None: + mlp_layers[-1].bias.data = torch.tensor(mlp_bias, requires_grad=True) + + if batch_norm_before_relu: + mlp_layers.append(BatchNorm1d(output_size)) + mlp_layers.append(Sigmoid() if idx == sigmoid_layer_idx else (create_activation_function(activation_function, bitwidth))) + input_size = output_size + if sigmoid_layer_idx == -1: + mlp_layers[-1] = Sigmoid() + return MLP(ModuleList(mlp_layers), name=name, shortcut_out_index=this_shortcut_out_index, shortcut_in_index=this_shortcut_in_index) + +def create_embeddings( + embedding_dimension: int, + layer_sizes: List[int], + weighted_pooling: Weighted_Pooling_Type, + binary_embedding: bool, + add_linear_to_binary_embeddings: bool, + sparse=False) -> Tuple[ModuleList, List[Union[None, torch.Tensor]]]: + """creates the embedding layers for each category.""" + if sparse: + logging.info("USING SPARSE EMBEDDINGS") + embedding_layers = ModuleList() + weighted_layers = [] + for layer_size in layer_sizes: + logging.info(f"creating embedding layer with {layer_size} * {embedding_dimension} = {layer_size * embedding_dimension} params...") + if binary_embedding: + embedding_layers.append(QEmbeddingBag( + layer_size, + embedding_dim=embedding_dimension, + mode="mean", + sparse=sparse, + use_linear_layer=add_linear_to_binary_embeddings + )) + else: + embedding_layers.append(EmbeddingBag(layer_size, embedding_dimension, mode="sum", sparse=sparse)) + embedding_weights = np.random.uniform( + low=-np.sqrt(1 / layer_size), high=np.sqrt(1 / layer_size), size=(layer_size, embedding_dimension) + ).astype(np.float32) + embedding_layers[-1].weight.data = torch.tensor(embedding_weights, requires_grad=True) + weighted_layers.append( + torch.ones(layer_size, dtype=torch.float32) + if not weighted_pooling == Weighted_Pooling_Type.NONE.value + else None) + + return embedding_layers, weighted_layers + + +class DLRM(Module): + total_size = 1.0 + inference_speed = 1.0 + validation_results = [] + + def __init__( + self, + dense_feature_size: int, + weighted_pooling: Weighted_Pooling_Type, + embedding_dimension: int, + embedding_layer_sizes: List[int], + bottom_mlp_layer_sizes: List[int], + bottom_sigmoid_layer_idx: int, + top_mlp_layer_sizes: List[int], + top_full_precision_layers: List[int], + bottom_full_precision_layers: List[int], + top_sigmoid_layer_idx: int, + interaction_operation: Interaction_Operation_Type, + binary_bottom_mlp: bool, + binary_top_mlp: bool, + batch_norm_before_relu: bool, + batch_norm_before_sign: bool, + binary_embedding: bool, + add_linear_to_binary_embeddings: bool, + activation_function: str, + bitwidth: int, + optimizer: str, + momentum: float, + lr: float, + lr_scheduler: str, + lr_factor: float, + lr_steps: List[int], + epochs: int, + loss: str, + loss_weights: Union[None, List[float]], + threshold: float, + shortcut: str = "none", + **kwargs) -> None: + super().__init__() + self.interaction_operation = interaction_operation + self.embedding_layers = create_embeddings( + embedding_dimension, + embedding_layer_sizes, + binary_embedding, + ) + + bottom_mlp_layer_sizes, self.sc_out_index = parse_layer_sizes(bottom_mlp_layer_sizes) + top_mlp_layer_sizes, self.sc_in_index = parse_layer_sizes(top_mlp_layer_sizes) + + # computing the correct bottom and top mlp layer sizes taking into account feature dimensions and feature interaction output shapes + bottom_mlp_layer_sizes = [dense_feature_size, *bottom_mlp_layer_sizes, embedding_dimension] + + if interaction_operation == Interaction_Operation_Type.CONCAT.value: + top_mlp_layer_sizes = [(len(embedding_layer_sizes) + 1) * embedding_dimension, *top_mlp_layer_sizes] + elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: + top_mlp_layer_sizes = [embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] + + self.bottom_mlp = create_mlp( + bottom_mlp_layer_sizes, + quantized=binary_bottom_mlp, + ) + self.top_mlp = create_mlp( + top_mlp_layer_sizes, + quantized=binary_top_mlp, + ) + self.loss_function, self.loss_weights = create_loss_function(loss, loss_weights) + + @staticmethod + def add_argparse_arguments(parent_parser: ArgumentParser): + parser = parent_parser.add_argument_group("DLRM Model") + parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64, 1]", + help="layer sizes of the bottom mlp") + parser.add_argument("--top-mlp-layer-sizes", type=str, default="[512, 256]", + # parser.add_argument("--top-mlp-layer-sizes", type=int, nargs="*", default=[48, 512, 256, 1], + help="layer sizes of the top mlp") + parser.add_argument("--bottom-sigmoid-layer-idx", type=int, default=None, + help="index of the sigmoid activation function in the bottom mlp (default is disabled, -1 is last)") + parser.add_argument("--top-sigmoid-layer-idx", type=int, default=-1, + help="index of the sigmoid activation function in the top mlp (None is disabled, -1 is last)") + parser.add_argument("--embedding-dimension", type=int, default=16, + help="number of embedding dimensions") + parser.add_argument("--interaction-operation", choices=[Interaction_Operation_Type.CONCAT.value, Interaction_Operation_Type.PRODUCT.value], default=Interaction_Operation_Type.CONCAT.value) + parser.add_argument("--weighted-pooling", choices=list(Weighted_Pooling_Type), default=Weighted_Pooling_Type.NONE.value) + parser.add_argument("--loss", type=str, default="mse", + help="name of loss function") + parser.add_argument('--loss-weights', nargs="*", default=None, + help='list loss weights. this is only used by bce loss') + parser.add_argument("--lr-scheduler", type=str, + choices=["cosine", "step", "exponential"], + help="name of the lr scheduler to use. default to none") + parser.add_argument("--lr", type=float, default=0.1, + help="initial learning rate (default: 0.1)") + parser.add_argument('--lr-factor', default=0.1, type=float, + help='learning rate decay ratio. this is used only by the step and exponential lr scheduler') + parser.add_argument('--lr-steps', nargs="*", default=[30, 60, 90], + help='list of learning rate decay epochs as list. this is used only by the step scheduler') + parser.add_argument('--momentum', type=float, default=0.9, + help='momentum value for optimizer, default is 0.9. only used for sgd optimizer') + parser.add_argument('--optimizer', type=str, default="sgd", choices=["adam", "sgd", "sparse_adam", "radam"], + help='the optimizer to use. default is adam.') + parser.add_argument("--threshold", type=float, default=0.5, help="threshold which is used to binarize predictions") + parser.add_argument("--lr-num-warmup-steps", type=int, default=0, help="number of warmups steps of the lr scheduler") + parser.add_argument("--lr-decay-start-step", type=int, default=0, help="number of steps until the lr decays") + parser.add_argument("--lr-num-decay-steps", type=int, default=0, help="number of steps how long the lr decays") + parser.add_argument("--full-embeddings", action="store_false", help="Disable sparse embeddings") + + parser.add_argument("--add-linear-to-binary-embeddings", action="store", type=str2bool, default=False, + help="whether to add an linear layer to binary embeddings") + parser.add_argument("--binary-embedding", action="store", type=str2bool, default=False, + help="toggles use of binary embeddings in model.") + parser.add_argument("--binary-top-mlp", action="store", type=str2bool, default=False, + help="toggles use of binary top mlp in model.") + parser.add_argument("--binary-bottom-mlp", action="store", type=str2bool, default=False, + help="toggles use of binary bottom mlp in model.") + parser.add_argument("--batch-norm-before-relu", action="store", type=str2bool, default=False, + help="toggles use of binary bottom mlp in model.") + parser.add_argument("--batch-norm-before-sign", action="store", type=str2bool, default=False, + help="toggles use of binary bottom mlp in model.") + parser.add_argument("--activation-function", choices=["relu", "prelu", "pact"], default="relu", type=str, help="select activation function") + parser.add_argument('--top-full-precision-layers', nargs="*", default=[], + help='list of learning rate decay epochs as list. this is used only by the step scheduler') + parser.add_argument('--bottom-full-precision-layers', nargs="*", default=[], + help='list of learning rate decay epochs as list. this is used only by the step scheduler') + return parent_parser + + def forward_embeddings(self, categorical_values_i: torch.Tensor, categorical_values_o: torch.Tensor) -> List[torch.Tensor]: + """forwards the preprocessed data through the embedding layers.""" + embedding_outputs = [] + for index, embedding_layer in enumerate(self.embedding_layers): + weight_pooling_layer = self.weight_pooling_layers[index] + index_group = categorical_values_i[index] + offset_group = categorical_values_o[index] + if weight_pooling_layer is not None: + per_sample_weights = weight_pooling_layer.gather(0, index_group) + else: + per_sample_weights = None + embedding_outputs.append(embedding_layer(index_group, offset_group, per_sample_weights=per_sample_weights)) + return embedding_outputs + + def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[torch.Tensor]): + if self.interaction_operation == Interaction_Operation_Type.PRODUCT.value: + batch_size, dimension = mlp_output.shape + concated_values = torch.cat([mlp_output] + embedding_outputs, dim=1).view((batch_size, -1, dimension)) + product_matrix = torch.bmm(concated_values, torch.transpose(concated_values, 1, 2)) + _, ni, nj = product_matrix.shape + li = torch.tensor([i for i in range(ni) for j in range(i + 0)]) + lj = torch.tensor([j for i in range(nj) for j in range(i + 0)]) + flat_product_matrix = product_matrix[:, li, lj] + result = torch.cat([mlp_output, flat_product_matrix], dim=1) + elif self.interaction_operation == Interaction_Operation_Type.CONCAT.value: + result = torch.cat([mlp_output] + embedding_outputs, dim=1) + else: + raise ValueError("Interaction operation not supported!") + + return result + + def forward(self, dense_values, categorical_values): + mlp_output, shortcout_output = self.bottom_mlp(dense_values) + embedding_outputs = self.forward_embeddings(*categorical_values) + feature_interactions = self.feature_interaction(mlp_output, embedding_outputs) + interaction_probability, _ = self.top_mlp(feature_interactions, shortcout_output) + + # if the top mlp has multiple output values, aggregate these into one single value + if len(interaction_probability.shape) > 1 and interaction_probability.shape[1] > 1: + interaction_probability = torch.clamp(interaction_probability, 0, 1) + interaction_probability = torch.mean(interaction_probability, dim=1) + return interaction_probability + + def training_step(self, batch, batch_idx): + dense_values, categorical_values_i, categorical_values_o, y = batch + if isinstance(categorical_values_i, list): + for el in categorical_values_i: + el.to(self.device) + else: + categorical_values_i = categorical_values_i.to(self.device) + if isinstance(categorical_values_o, list): + for el in categorical_values_o: + el.to(self.device) + else: + categorical_values_o = categorical_values_o.to(self.device) + dense_values.to(self.device) + y_hat = self(dense_values, (categorical_values_i, categorical_values_o)) + + loss = self.loss_function(torch.squeeze(y_hat), torch.squeeze(y)) + self.log_dict({ "loss": loss }) + return loss + + def validation_step_end(self, *args, **kwargs): + """calculate all them metrics and log via wandb/tensorboard""" + + y = torch.cat(list(map(lambda x: x["y"], self.validation_results))) + y_hat = torch.cat(list(map(lambda x: x["y_hat"], self.validation_results))) + loss = self.loss_function(y, y_hat) + rmse = torch.sqrt(F.mse_loss(y_hat, y)).item() + y_array = np.array(y.cpu()) + y_hat_array = np.array(y_hat.cpu()) >= self.hparams.threshold + balanced_accuracy = metrics.balanced_accuracy_score(y_array, y_hat_array) + accuracy = metrics.accuracy_score(y_array, y_hat_array) + f1 = metrics.f1_score(y_array, y_hat_array) + roc_auc = metrics.roc_auc_score(y_array, y_hat.cpu()) + precision = metrics.precision_score(y_array, y_hat_array) + recall = metrics.recall_score(y_array, y_hat_array) + self.log_dict({ + "val_los": loss, + "val_rmse": rmse, + "roc_auc": roc_auc, + "precision": precision, + "recall": recall, + "balanced accuracy": balanced_accuracy, + "accuracy": accuracy, + "f1 score": f1, + "model size": self.total_size, + "inference speed": self.inference_speed, + "weighted accuracy": accuracy / (self.total_size * self.inference_speed), + "weighted speed accuracy": accuracy / (self.total_size * self.inference_speed), + "log2 weighted speed accuracy": accuracy / math.log(self.total_size * self.inference_speed, 2.0) + }, prog_bar=True) + return super().validation_step_end(*args, **kwargs) + + def on_validation_start(self) -> None: + self.validation_results = [] + return super().on_validation_start() + + def validation_step(self, batch, batch_idx): + dense_values, categorical_values_i, categorical_values_o, y = batch + dense_values = dense_values.to(self.device) + if isinstance(categorical_values_i, list): + for el in categorical_values_i: + el.to(self.device) + else: + categorical_values_i = categorical_values_i.to(self.device) + if isinstance(categorical_values_o, list): + for el in categorical_values_o: + el.to(self.device) + else: + categorical_values_o = categorical_values_o.to(self.device) + y_hat = torch.squeeze(self(dense_values, (categorical_values_i, categorical_values_o))) + y = torch.squeeze(y) + y_hat = torch.squeeze(y_hat) + self.validation_results.append({ "y": y, "y_hat": y_hat }) + + def configure_optimizers(self): + configuration = {} + optimizer = create_optimizer(self.hparams.optimizer, self, self.hparams.lr, self.hparams.momentum) + configuration["optimizer"] = optimizer + lr_scheduler = LRPolicyScheduler( + optimizer, + self.hparams.lr_num_warmup_steps, + self.hparams.lr_decay_start_step, + self.hparams.lr_num_decay_steps, + ) + if lr_scheduler != None: + configuration["lr_scheduler"] = lr_scheduler_config = { + # REQUIRED: The scheduler instance + "scheduler": lr_scheduler, + # The unit of the scheduler's step size, could also be 'step'. + # 'epoch' updates the scheduler on epoch end whereas 'step' + # updates it after a optimizer update. + "interval": "step", + # How many epochs/steps should pass between calls to + # `scheduler.step()`. 1 corresponds to updating the learning + # rate after every epoch/step. + "frequency": 1, + # Metric to to monitor for schedulers like `ReduceLROnPlateau` + "monitor": "balanced accuracy", + # If set to `True`, will enforce that the value specified 'monitor' + # is available when the scheduler is updated, thus stopping + # training if not found. If set to `False`, it will only produce a warning + "strict": True, + # If using the `LearningRateMonitor` callback to monitor the + # learning rate progress, this keyword can be used to specify + # a custom logged name + "name": "LRScheduler", + } + return configuration + + def transform_categorical(self, categorical: List[torch.Tensor]): + categorical_batch_indices = [[torch.tensor(*np.where(category_batch[batch_num].numpy())) for batch_num in range(category_batch.size(0))] for category_batch in categorical] + offsets = [torch.tensor([len(index_array) for index_array in category_batch]) for category_batch in categorical_batch_indices] + for category_index, category_sizes in enumerate(offsets): + offsets[category_index] = offsets[category_index].roll(1) + offsets[category_index][0] = 0 + for size_index in range(1, len(category_sizes)): + offsets[category_index][size_index] += offsets[category_index][size_index - 1] + + concatenated_categorical_batch_indices = [torch.cat(category_batch) for category_batch in categorical_batch_indices] + return list(zip(concatenated_categorical_batch_indices, offsets)) From 030f16abdc6203c5fd968fa314483079ffaa3fd2 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 1 Jul 2022 11:16:20 +0200 Subject: [PATCH 071/208] formatted code --- bitorch/models/dlrm.py | 141 +++++++++++++++++++++++++++-------------- requirements-dev.txt | 1 + 2 files changed, 96 insertions(+), 46 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 6a26728..0a6562f 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -22,20 +22,25 @@ # from .utils import create_loss_function, create_optimizer, create_activation_function, parse_layer_sizes, str2bool from bitorch.layers.qembedding import QEmbeddingBag + class Weighted_Pooling_Type(Enum): NONE = 0 FIXED = 1 LEARNED = 2 + class Interaction_Operation_Type(Enum): PRODUCT = "product" CONCAT = "concat" SUM = "sum" + class MLP(torch.nn.Module): """Mlp class for DLRM mlps. this mainly is used to properly pass shortcut data""" name = "MLP" - def __init__(self, layers: ModuleList, name: str = "MLP", shortcut_out_index: Union[int, None] = None, shortcut_in_index: Union[int, None] = None): + + def __init__(self, layers: ModuleList, name: str = "MLP", + shortcut_out_index: Union[int, None] = None, shortcut_in_index: Union[int, None] = None): super().__init__() self.shortcut_out_index = shortcut_out_index self.shortcut_in_index = shortcut_in_index @@ -44,7 +49,7 @@ def __init__(self, layers: ModuleList, name: str = "MLP", shortcut_out_index: Un logging.info(f"shortcut_out_index {shortcut_out_index}") logging.info(f"shortcut_in_index {shortcut_in_index}") - def forward(self, X, shortcut_value = None): + def forward(self, X, shortcut_value=None): out = None Y = X for index, layer in enumerate(self.layers): @@ -54,16 +59,17 @@ def forward(self, X, shortcut_value = None): if index == self.shortcut_out_index: out = Y return Y, out - + def __str__(self) -> str: out = f"{self.name}\n" - out += "="*10 + out += "=" * 10 out += "\n".join(self.layers) return out + def create_mlp( - layer_sizes: List[int], - quantized: bool = False) -> MLP: + layer_sizes: List[int], + quantized: bool = False) -> MLP: """creates a mlp module Args: @@ -103,11 +109,19 @@ def create_mlp( if batch_norm_before_relu: mlp_layers.append(BatchNorm1d(output_size)) - mlp_layers.append(Sigmoid() if idx == sigmoid_layer_idx else (create_activation_function(activation_function, bitwidth))) + mlp_layers.append( + Sigmoid() if idx == sigmoid_layer_idx else ( + create_activation_function( + activation_function, bitwidth))) input_size = output_size if sigmoid_layer_idx == -1: mlp_layers[-1] = Sigmoid() - return MLP(ModuleList(mlp_layers), name=name, shortcut_out_index=this_shortcut_out_index, shortcut_in_index=this_shortcut_in_index) + return MLP( + ModuleList(mlp_layers), + name=name, + shortcut_out_index=this_shortcut_out_index, + shortcut_in_index=this_shortcut_in_index) + def create_embeddings( embedding_dimension: int, @@ -122,7 +136,8 @@ def create_embeddings( embedding_layers = ModuleList() weighted_layers = [] for layer_size in layer_sizes: - logging.info(f"creating embedding layer with {layer_size} * {embedding_dimension} = {layer_size * embedding_dimension} params...") + logging.info( + f"creating embedding layer with {layer_size} * {embedding_dimension} = {layer_size * embedding_dimension} params...") if binary_embedding: embedding_layers.append(QEmbeddingBag( layer_size, @@ -134,14 +149,14 @@ def create_embeddings( else: embedding_layers.append(EmbeddingBag(layer_size, embedding_dimension, mode="sum", sparse=sparse)) embedding_weights = np.random.uniform( - low=-np.sqrt(1 / layer_size), high=np.sqrt(1 / layer_size), size=(layer_size, embedding_dimension) - ).astype(np.float32) + low=-np.sqrt(1 / layer_size), high=np.sqrt(1 / layer_size), size=(layer_size, embedding_dimension) + ).astype(np.float32) embedding_layers[-1].weight.data = torch.tensor(embedding_weights, requires_grad=True) weighted_layers.append( torch.ones(layer_size, dtype=torch.float32) if not weighted_pooling == Weighted_Pooling_Type.NONE.value else None) - + return embedding_layers, weighted_layers @@ -194,13 +209,15 @@ def __init__( bottom_mlp_layer_sizes, self.sc_out_index = parse_layer_sizes(bottom_mlp_layer_sizes) top_mlp_layer_sizes, self.sc_in_index = parse_layer_sizes(top_mlp_layer_sizes) - # computing the correct bottom and top mlp layer sizes taking into account feature dimensions and feature interaction output shapes + # computing the correct bottom and top mlp layer sizes taking into account + # feature dimensions and feature interaction output shapes bottom_mlp_layer_sizes = [dense_feature_size, *bottom_mlp_layer_sizes, embedding_dimension] if interaction_operation == Interaction_Operation_Type.CONCAT.value: top_mlp_layer_sizes = [(len(embedding_layer_sizes) + 1) * embedding_dimension, *top_mlp_layer_sizes] elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: - top_mlp_layer_sizes = [embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] + top_mlp_layer_sizes = [ + embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] self.bottom_mlp = create_mlp( bottom_mlp_layer_sizes, @@ -216,61 +233,90 @@ def __init__( def add_argparse_arguments(parent_parser: ArgumentParser): parser = parent_parser.add_argument_group("DLRM Model") parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64, 1]", - help="layer sizes of the bottom mlp") + help="layer sizes of the bottom mlp") parser.add_argument("--top-mlp-layer-sizes", type=str, default="[512, 256]", - # parser.add_argument("--top-mlp-layer-sizes", type=int, nargs="*", default=[48, 512, 256, 1], - help="layer sizes of the top mlp") - parser.add_argument("--bottom-sigmoid-layer-idx", type=int, default=None, + # parser.add_argument("--top-mlp-layer-sizes", type=int, nargs="*", + # default=[48, 512, 256, 1], + help="layer sizes of the top mlp") + parser.add_argument( + "--bottom-sigmoid-layer-idx", + type=int, + default=None, help="index of the sigmoid activation function in the bottom mlp (default is disabled, -1 is last)") - parser.add_argument("--top-sigmoid-layer-idx", type=int, default=-1, + parser.add_argument( + "--top-sigmoid-layer-idx", + type=int, + default=-1, help="index of the sigmoid activation function in the top mlp (None is disabled, -1 is last)") parser.add_argument("--embedding-dimension", type=int, default=16, - help="number of embedding dimensions") - parser.add_argument("--interaction-operation", choices=[Interaction_Operation_Type.CONCAT.value, Interaction_Operation_Type.PRODUCT.value], default=Interaction_Operation_Type.CONCAT.value) - parser.add_argument("--weighted-pooling", choices=list(Weighted_Pooling_Type), default=Weighted_Pooling_Type.NONE.value) + help="number of embedding dimensions") + parser.add_argument( + "--interaction-operation", + choices=[ + Interaction_Operation_Type.CONCAT.value, + Interaction_Operation_Type.PRODUCT.value], + default=Interaction_Operation_Type.CONCAT.value) + parser.add_argument( + "--weighted-pooling", + choices=list(Weighted_Pooling_Type), + default=Weighted_Pooling_Type.NONE.value) parser.add_argument("--loss", type=str, default="mse", - help="name of loss function") + help="name of loss function") parser.add_argument('--loss-weights', nargs="*", default=None, help='list loss weights. this is only used by bce loss') parser.add_argument("--lr-scheduler", type=str, - choices=["cosine", "step", "exponential"], - help="name of the lr scheduler to use. default to none") + choices=["cosine", "step", "exponential"], + help="name of the lr scheduler to use. default to none") parser.add_argument("--lr", type=float, default=0.1, help="initial learning rate (default: 0.1)") - parser.add_argument('--lr-factor', default=0.1, type=float, - help='learning rate decay ratio. this is used only by the step and exponential lr scheduler') + parser.add_argument( + '--lr-factor', + default=0.1, + type=float, + help='learning rate decay ratio. this is used only by the step and exponential lr scheduler') parser.add_argument('--lr-steps', nargs="*", default=[30, 60, 90], help='list of learning rate decay epochs as list. this is used only by the step scheduler') parser.add_argument('--momentum', type=float, default=0.9, help='momentum value for optimizer, default is 0.9. only used for sgd optimizer') parser.add_argument('--optimizer', type=str, default="sgd", choices=["adam", "sgd", "sparse_adam", "radam"], help='the optimizer to use. default is adam.') - parser.add_argument("--threshold", type=float, default=0.5, help="threshold which is used to binarize predictions") - parser.add_argument("--lr-num-warmup-steps", type=int, default=0, help="number of warmups steps of the lr scheduler") + parser.add_argument("--threshold", type=float, default=0.5, + help="threshold which is used to binarize predictions") + parser.add_argument("--lr-num-warmup-steps", type=int, default=0, + help="number of warmups steps of the lr scheduler") parser.add_argument("--lr-decay-start-step", type=int, default=0, help="number of steps until the lr decays") parser.add_argument("--lr-num-decay-steps", type=int, default=0, help="number of steps how long the lr decays") parser.add_argument("--full-embeddings", action="store_false", help="Disable sparse embeddings") - parser.add_argument("--add-linear-to-binary-embeddings", action="store", type=str2bool, default=False, - help="whether to add an linear layer to binary embeddings") + parser.add_argument("--add-linear-to-binary-embeddings", action="store", type=str2bool, default=False, + help="whether to add an linear layer to binary embeddings") parser.add_argument("--binary-embedding", action="store", type=str2bool, default=False, - help="toggles use of binary embeddings in model.") + help="toggles use of binary embeddings in model.") parser.add_argument("--binary-top-mlp", action="store", type=str2bool, default=False, - help="toggles use of binary top mlp in model.") + help="toggles use of binary top mlp in model.") parser.add_argument("--binary-bottom-mlp", action="store", type=str2bool, default=False, - help="toggles use of binary bottom mlp in model.") + help="toggles use of binary bottom mlp in model.") parser.add_argument("--batch-norm-before-relu", action="store", type=str2bool, default=False, - help="toggles use of binary bottom mlp in model.") + help="toggles use of binary bottom mlp in model.") parser.add_argument("--batch-norm-before-sign", action="store", type=str2bool, default=False, - help="toggles use of binary bottom mlp in model.") - parser.add_argument("--activation-function", choices=["relu", "prelu", "pact"], default="relu", type=str, help="select activation function") + help="toggles use of binary bottom mlp in model.") + parser.add_argument( + "--activation-function", + choices=[ + "relu", + "prelu", + "pact"], + default="relu", + type=str, + help="select activation function") parser.add_argument('--top-full-precision-layers', nargs="*", default=[], help='list of learning rate decay epochs as list. this is used only by the step scheduler') parser.add_argument('--bottom-full-precision-layers', nargs="*", default=[], help='list of learning rate decay epochs as list. this is used only by the step scheduler') return parent_parser - def forward_embeddings(self, categorical_values_i: torch.Tensor, categorical_values_o: torch.Tensor) -> List[torch.Tensor]: + def forward_embeddings(self, categorical_values_i: torch.Tensor, + categorical_values_o: torch.Tensor) -> List[torch.Tensor]: """forwards the preprocessed data through the embedding layers.""" embedding_outputs = [] for index, embedding_layer in enumerate(self.embedding_layers): @@ -298,7 +344,7 @@ def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[ result = torch.cat([mlp_output] + embedding_outputs, dim=1) else: raise ValueError("Interaction operation not supported!") - + return result def forward(self, dense_values, categorical_values): @@ -329,9 +375,9 @@ def training_step(self, batch, batch_idx): y_hat = self(dense_values, (categorical_values_i, categorical_values_o)) loss = self.loss_function(torch.squeeze(y_hat), torch.squeeze(y)) - self.log_dict({ "loss": loss }) + self.log_dict({"loss": loss}) return loss - + def validation_step_end(self, *args, **kwargs): """calculate all them metrics and log via wandb/tensorboard""" @@ -384,7 +430,7 @@ def validation_step(self, batch, batch_idx): y_hat = torch.squeeze(self(dense_values, (categorical_values_i, categorical_values_o))) y = torch.squeeze(y) y_hat = torch.squeeze(y_hat) - self.validation_results.append({ "y": y, "y_hat": y_hat }) + self.validation_results.append({"y": y, "y_hat": y_hat}) def configure_optimizers(self): configuration = {} @@ -396,7 +442,7 @@ def configure_optimizers(self): self.hparams.lr_decay_start_step, self.hparams.lr_num_decay_steps, ) - if lr_scheduler != None: + if lr_scheduler is not None: configuration["lr_scheduler"] = lr_scheduler_config = { # REQUIRED: The scheduler instance "scheduler": lr_scheduler, @@ -422,13 +468,16 @@ def configure_optimizers(self): return configuration def transform_categorical(self, categorical: List[torch.Tensor]): - categorical_batch_indices = [[torch.tensor(*np.where(category_batch[batch_num].numpy())) for batch_num in range(category_batch.size(0))] for category_batch in categorical] - offsets = [torch.tensor([len(index_array) for index_array in category_batch]) for category_batch in categorical_batch_indices] + categorical_batch_indices = [[torch.tensor(*np.where(category_batch[batch_num].numpy())) + for batch_num in range(category_batch.size(0))] for category_batch in categorical] + offsets = [torch.tensor([len(index_array) for index_array in category_batch]) + for category_batch in categorical_batch_indices] for category_index, category_sizes in enumerate(offsets): offsets[category_index] = offsets[category_index].roll(1) offsets[category_index][0] = 0 for size_index in range(1, len(category_sizes)): offsets[category_index][size_index] += offsets[category_index][size_index - 1] - concatenated_categorical_batch_indices = [torch.cat(category_batch) for category_batch in categorical_batch_indices] + concatenated_categorical_batch_indices = [ + torch.cat(category_batch) for category_batch in categorical_batch_indices] return list(zip(concatenated_categorical_batch_indices, offsets)) diff --git a/requirements-dev.txt b/requirements-dev.txt index 66e7f4f..f2210cf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ build flake8 mypy +pep8 pep8-naming pytest sphinx From e5cda647b45975afd55e7b49c087d1cd7948dfc9 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 20 May 2022 07:39:46 +0200 Subject: [PATCH 072/208] added readthedocs configuration file --- .readthedocs.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..ffa047b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.9" + # You can also specify other tool versions: + # nodejs: "16" + # rust: "1.55" + # golang: "1.17" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: requirements-dev.txt From 8fed1890d398f317508bd71886a31b4955d84fd2 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 20 May 2022 14:49:41 +0200 Subject: [PATCH 073/208] some changes --- .readthedocs.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ffa047b..f441005 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -17,7 +17,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/conf.py + configuration: docs/source/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: @@ -26,4 +26,5 @@ sphinx: # Optionally declare the Python requirements required to build your docs python: install: + - requirements: requirements.txt - requirements: requirements-dev.txt From 47f97deec546a0bcdc5b0d91894a134808ef8932 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 20 May 2022 14:55:27 +0200 Subject: [PATCH 074/208] render separate page for each class again --- docs/source/_templates/module.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/_templates/module.rst b/docs/source/_templates/module.rst index 4ce24de..b3c490f 100644 --- a/docs/source/_templates/module.rst +++ b/docs/source/_templates/module.rst @@ -33,6 +33,7 @@ .. autosummary:: :template: class.rst + :toctree: {% for item in classes %} {{ item }} {%- endfor %} From c158132828650e6e64e51f470ee0f09a70d0fa3b Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 20 May 2022 15:05:25 +0200 Subject: [PATCH 075/208] changed links in doc --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45fc432..e7ca89b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Our current roadmap contains: All changes are tracked in the [changelog](https://github.com/hpi-xnor/bitorch/blob/main/CHANGELOG.md). +Please refer to [our wiki](https://bitorch.readthedocs.io/en/latest/) for a comprehensive introduction into +the library or use the introduction notebook in `examples/notebooks`. + ## Installation Similar to recent versions of [torchvision](https://github.com/pytorch/vision), you should be using Python 3.8 or newer. @@ -18,7 +21,7 @@ Currently, the only supported installation is pip (a conda package is planned in ### Pip -If you wish to use a *specific version* of PyTorch for compatibility with certain devices or CUDA versions, +If you wish to use a _specific version_ of PyTorch for compatibility with certain devices or CUDA versions, we advise on installing the corresponding versions of `pytorch` and `torchvision` first (or afterwards), please consult [pytorch's getting started guide](https://pytorch.org/get-started/locally/). @@ -33,7 +36,6 @@ pip install bitorch --extra-index-url https://download.pytorch.org/whl/cu113 ``` If you want to run the examples install the optional dependencies as well: - ```bash pip install "bitorch[opt]" ``` From 55594a4ba1d9f27cff79e0f6e625171cb30b9cda Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 5 Jul 2022 16:03:14 +0200 Subject: [PATCH 076/208] add knowledge distillation --- examples/mnist/train_mnist.py | 2 - .../pytorch_lightning/image_classification.py | 18 +++++++-- examples/pytorch_lightning/requirements.txt | 1 - .../pytorch_lightning/utils/arg_parser.py | 20 ++++++++++ examples/pytorch_lightning/utils/kd_loss.py | 37 +++++++++++++++++++ .../utils/lightning_model.py | 26 +++++++++++-- examples/pytorch_lightning/utils/teachers.py | 36 ++++++++++++++++++ requirements.txt | 4 +- 8 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 examples/pytorch_lightning/utils/kd_loss.py create mode 100644 examples/pytorch_lightning/utils/teachers.py diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 544fe8e..0d7ec46 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -44,7 +44,6 @@ def forward(self, x): x = self.bn1(x) x = self.fc2(x) - x = x.to(dtype=torch.float32) # todo: fix in inference engine x = self.act2(x) x = self.bn2(x) @@ -164,7 +163,6 @@ def main(): if args.model == "mlp": model = QuantizedMLP().to(device) - print(model) else: model = Net().to(device) optimizer = optim.Adadelta(model.parameters(), lr=args.lr) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 4ee37b5..ccaf814 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -13,7 +13,7 @@ import argparse import logging from pathlib import Path -from typing import List, Any +from typing import List, Any, Type import fvbitcore.nn as fv_nn import torch @@ -30,7 +30,7 @@ from bitorch.quantizations import Quantization from examples.pytorch_lightning.utils.log import CommandLineLogger from utils.arg_parser import create_argparser -from utils.lightning_model import ModelWrapper +from utils.lightning_model import ModelWrapper, DistillationModelWrapper from utils.utils import configure_logging logger = logging.getLogger() @@ -91,11 +91,21 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model = model_from_name(args.model)(**model_kwargs, dataset=dataset) # type: ignore model.initialize() + + wrapper_class: Type[ModelWrapper] = ModelWrapper + if args.teacher: + if args.dataset != "imagenet": + raise ValueError( + f"Teacher '{args.teacher}' and dataset '{args.dataset}' selected, " + f"but teacher models are only supported for imagenet." + ) + wrapper_class = DistillationModelWrapper + if args.checkpoint_load is not None and args.pretrained: logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") - model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) + model_wrapped = wrapper_class.load_from_checkpoint(args.checkpoint_load) else: - model_wrapped = ModelWrapper(model, dataset.num_classes, args) + model_wrapped = wrapper_class(model, dataset.num_classes, args) trainer = Trainer( strategy=args.strategy, diff --git a/examples/pytorch_lightning/requirements.txt b/examples/pytorch_lightning/requirements.txt index 7308aef..66db890 100644 --- a/examples/pytorch_lightning/requirements.txt +++ b/examples/pytorch_lightning/requirements.txt @@ -2,5 +2,4 @@ bitorch fvbitcore pytorch_lightning sklearn -torchmetrics wandb diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 46aa837..84455f0 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -7,6 +7,8 @@ from bitorch import add_config_args from pytorch_lightning import Trainer +from examples.pytorch_lightning.utils.teachers import available_teachers + def add_logging_args(parser: ArgumentParser) -> None: """adds cli parameters for logging configuration @@ -157,6 +159,23 @@ def add_optimizer_args(parser: ArgumentParser) -> None: ) +def add_training_args(parser: ArgumentParser) -> None: + """ + Add arguments for training strategies. + + Args: + parser (ArgumentParser): the main argument parser + """ + train = parser.add_argument_group("training", "parameters for the training strategies") + train.add_argument( + "--teacher", + type=str, + default="", + choices=available_teachers(), + help="name of the teacher model, the student is going to be trained with KD if not empty", + ) + + def add_dataset_args(parser: ArgumentParser) -> None: """adds cli parameters for dataset configuration @@ -257,6 +276,7 @@ def add_regular_args(parser: ArgumentParser) -> None: add_dataset_args(parser) add_optimizer_args(parser) add_checkpoint_args(parser) + add_training_args(parser) add_config_args(parser) diff --git a/examples/pytorch_lightning/utils/kd_loss.py b/examples/pytorch_lightning/utils/kd_loss.py new file mode 100644 index 0000000..7a17d1b --- /dev/null +++ b/examples/pytorch_lightning/utils/kd_loss.py @@ -0,0 +1,37 @@ +# Code is modified from MEAL (https://arxiv.org/abs/1812.02425) and Label Refinery (https://arxiv.org/abs/1805.02641). + +import torch +from torch.nn import functional as F +from torch.nn.modules import loss + + +class DistributionLoss(loss._Loss): + """The KL-Divergence loss for a student and teacher model.""" + + def forward(self, student_out: torch.Tensor, teacher_out: torch.Tensor) -> torch.Tensor: + """ + Calculate the KL-Divergence loss. + + Args: + student_out: NxC tensor (must be the output of the student network before softmax function) + teacher_out: NxC tensor (each row must be a probability score, adding up to one) + + Returns: + the loss score + """ + # check that teacher does not require gradients + if teacher_out.requires_grad: + raise ValueError("real network output should not require gradients.") + + student_log_prob = F.log_softmax(student_out, dim=1) + teacher_soft_output = F.softmax(teacher_out, dim=1) + del student_out, teacher_out + + # Loss is -dot(student_log_prob, teacher_out). Reshape tensors for batch matrix multiplication + teacher_soft_output = teacher_soft_output.unsqueeze(1) + student_log_prob = student_log_prob.unsqueeze(2) + + # Compute the loss, and average for the batch. + cross_entropy_loss = -torch.bmm(teacher_soft_output, student_log_prob) + + return cross_entropy_loss.mean() diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 19a7ce9..a723889 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -7,6 +7,8 @@ from torch.nn import Module, CrossEntropyLoss from torchmetrics import Accuracy, F1Score, Precision, Recall +from .kd_loss import DistributionLoss +from .teachers import get_teacher from .unused_args import clean_hyperparameters from .utils import create_optimizer, create_scheduler @@ -16,11 +18,11 @@ def __init__( self, model: Module, num_classes: int, - args: Namespace, + script_args: Namespace, add_f1_prec_recall: bool = False, ) -> None: super().__init__() - self.save_hyperparameters(clean_hyperparameters(args)) + self.save_hyperparameters(clean_hyperparameters(script_args)) self.loss_function = CrossEntropyLoss() self.model = model self.train_accuracy_top1 = Accuracy(num_classes=num_classes) @@ -37,7 +39,7 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore x_train, y_train = batch y_hat = self.model(x_train) - loss = self.loss_function(y_hat, y_train) + loss = self.calculate_loss(x_train, y_train, y_hat) self.train_accuracy_top1(y_hat, y_train) self.train_accuracy_top5(y_hat, y_train) self.log_dict( @@ -52,6 +54,9 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore self.log("loss/train", loss, on_step=True, on_epoch=False) return loss + def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor): + return self.loss_function(y_hat, y_train) + def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: ignore x_test, y_test = batch @@ -96,3 +101,18 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i return {"optimizer": optimizer, "lr_scheduler": scheduler} else: return optimizer + + +class DistillationModelWrapper(ModelWrapper): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._kd_loss = DistributionLoss() + self.teacher = get_teacher(self.hparams.teacher) + # self.teacher.eval() + for param in self.teacher.parameters(): + param.requires_grad = False + + def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor): + y_hat_student = self.forward(x_train) + y_hat_teacher = self.teacher.forward(x_train) + return self._kd_loss(y_hat_student, y_hat_teacher) diff --git a/examples/pytorch_lightning/utils/teachers.py b/examples/pytorch_lightning/utils/teachers.py new file mode 100644 index 0000000..99ced23 --- /dev/null +++ b/examples/pytorch_lightning/utils/teachers.py @@ -0,0 +1,36 @@ +from typing import Dict, List + +from torch import nn + + +from torchvision import models + + +def _teachers() -> Dict[str, nn.Module]: + def resnet18(): + return models.resnet18(weights=models.ResNet18_Weights.DEFAULT) + + def resnet34(): + return models.resnet34(weights=models.ResNet34_Weights.DEFAULT) + + def resnet50_v1(): + # Old weights with accuracy 76.130% + return models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) + + def resnet50_v2(): + # New weights with accuracy 80.858% + return models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) + + def resnet50(): + # New weights with accuracy 80.858% + return models.resnet50(weights=models.ResNet50_Weights.DEFAULT) + + return locals() + + +def available_teachers() -> List[str]: + return list(_teachers().keys()) + + +def get_teacher(teacher_name: str) -> nn.Module: + return _teachers()[teacher_name]() diff --git a/requirements.txt b/requirements.txt index 1017d57..f34e435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -torch -torchvision +torch~=1.12.0 +torchvision~=0.13.0 matplotlib numpy From 9fe05896085da9bf2121bab10e3bcff68a7a2684 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 5 Jul 2022 16:50:40 +0200 Subject: [PATCH 077/208] limit opt requirements to pytorch lightning examples --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e82c4ef..d67aaaf 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,8 @@ def _get_files_recursively(glob: str, root: str = ".") -> List[str]: install_requires=_get_requirements("requirements.txt"), extras_require={ "dev": _get_requirements("requirements-dev.txt"), - "opt": _get_requirements(*_get_files_recursively("requirements*.txt", root="examples")), + # "opt": _get_requirements(*_get_files_recursively("requirements*.txt", root="examples")), + "opt": _get_requirements("examples/pytorch_lightning/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", From a6107804bd4936febd7c618271cac44b2991e2da Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 5 Jul 2022 17:51:23 +0200 Subject: [PATCH 078/208] allow pickling of layers --- bitorch/layers/qconv1d.py | 6 +- bitorch/layers/qconv2d.py | 6 +- bitorch/layers/qconv3d.py | 6 +- bitorch/layers/qlinear.py | 6 +- .../pytorch_lightning/image_classification.py | 5 +- .../utils/lightning_model.py | 4 +- examples/pytorch_lightning/utils/teachers.py | 10 +-- tests/layers/test_layer_impl.py | 73 +++++++++++-------- tests/layers/test_switchable_layer.py | 2 + 9 files changed, 70 insertions(+), 48 deletions(-) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 856f8f5..140ba8d 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -112,8 +112,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -@QConv1dImplementation(RuntimeMode.DEFAULT) -class QConv1d(DefaultImplementationMixin, QConv1dBase): +class QConv1dComposed(DefaultImplementationMixin, QConv1dBase): """ This class defines the default implementation of a QConv1d layer (which is actually implemented by QConv1dBase). @@ -121,3 +120,6 @@ class QConv1d(DefaultImplementationMixin, QConv1dBase): """ pass + + +QConv1d = QConv1dImplementation(RuntimeMode.DEFAULT)(QConv1dComposed) diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 908030d..578721e 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -106,8 +106,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -@QConv2dImplementation(RuntimeMode.DEFAULT) -class QConv2d(DefaultImplementationMixin, QConv2dBase): +class QConv2dComposed(DefaultImplementationMixin, QConv2dBase): """ This class defines the default implementation of a QConv2d layer (which is actually implemented by QConv2dBase). @@ -115,3 +114,6 @@ class QConv2d(DefaultImplementationMixin, QConv2dBase): """ pass + + +QConv2d = QConv2dImplementation(RuntimeMode.DEFAULT)(QConv2dComposed) diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 4bcb13f..88329d2 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -106,8 +106,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -@QConv3dImplementation(RuntimeMode.DEFAULT) -class QConv3d(DefaultImplementationMixin, QConv3dBase): +class QConv3dComposed(DefaultImplementationMixin, QConv3dBase): """ This class defines the default implementation of a QConv3d layer (which is actually implemented by QConv3dBase). @@ -115,3 +114,6 @@ class QConv3d(DefaultImplementationMixin, QConv3dBase): """ pass + + +QConv3d = QConv3dImplementation(RuntimeMode.DEFAULT)(QConv3dComposed) diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 40e4ba5..8903253 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -78,8 +78,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return linear(self.activation(x), self.weight_quantization(self.weight), self.bias) -@QLinearImplementation(RuntimeMode.DEFAULT) -class QLinear(DefaultImplementationMixin, QLinearBase): +class QLinearComposed(DefaultImplementationMixin, QLinearBase): """ This class defines the default implementation of a QLinear layer (which is actually implemented by QLinearBase). @@ -87,3 +86,6 @@ class QLinear(DefaultImplementationMixin, QLinearBase): """ pass + + +QLinear = QLinearImplementation(RuntimeMode.DEFAULT)(QLinearComposed) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index ccaf814..0cd8de5 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -23,7 +23,8 @@ from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase, WandbLogger from torch.utils.data import DataLoader -from bitorch import apply_args_to_configuration +import bitorch +from bitorch import apply_args_to_configuration, RuntimeMode from bitorch.datasets import dataset_from_name from bitorch.datasets.base import Augmentation from bitorch.models import model_from_name @@ -45,6 +46,8 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: """ configure_logging(logger, args.log_file, args.log_level, args.log_stdout) + bitorch.mode = RuntimeMode.RAW + apply_args_to_configuration(args) output_dir = Path(args.result_directory) diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index a723889..8647b1a 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -104,7 +104,7 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i class DistillationModelWrapper(ModelWrapper): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._kd_loss = DistributionLoss() self.teacher = get_teacher(self.hparams.teacher) @@ -112,7 +112,7 @@ def __init__(self, *args, **kwargs): for param in self.teacher.parameters(): param.requires_grad = False - def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor): + def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor) -> torch.Tensor: y_hat_student = self.forward(x_train) y_hat_teacher = self.teacher.forward(x_train) return self._kd_loss(y_hat_student, y_hat_teacher) diff --git a/examples/pytorch_lightning/utils/teachers.py b/examples/pytorch_lightning/utils/teachers.py index 99ced23..a13cb07 100644 --- a/examples/pytorch_lightning/utils/teachers.py +++ b/examples/pytorch_lightning/utils/teachers.py @@ -7,21 +7,21 @@ def _teachers() -> Dict[str, nn.Module]: - def resnet18(): + def resnet18() -> nn.Module: return models.resnet18(weights=models.ResNet18_Weights.DEFAULT) - def resnet34(): + def resnet34() -> nn.Module: return models.resnet34(weights=models.ResNet34_Weights.DEFAULT) - def resnet50_v1(): + def resnet50_v1() -> nn.Module: # Old weights with accuracy 76.130% return models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) - def resnet50_v2(): + def resnet50_v2() -> nn.Module: # New weights with accuracy 80.858% return models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) - def resnet50(): + def resnet50() -> nn.Module: # New weights with accuracy 80.858% return models.resnet50(weights=models.ResNet50_Weights.DEFAULT) diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_impl.py index a70c27b..d34eb4b 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_impl.py @@ -1,3 +1,4 @@ +import pickle from typing import Any import pytest @@ -12,7 +13,7 @@ TEST_MODE = RuntimeMode.INFERENCE_AUTO -class LayerBase: +class ExampleBase: def __init__(self, s: str, val: int = 42) -> None: self.s = s self.val = val @@ -21,26 +22,29 @@ def do_something(self): return f"{self.s}: {self.val} - made by {self.class_name()}" def class_name(self) -> str: - return self.__class__.__name__ + return "BaseClass" -test_registry = LayerRegistry("Layer") +example_registry = LayerRegistry("Example") -class LayerImplementation(LayerImplementation): +class ExampleImplementation(LayerImplementation): def __init__(self, *args): - super().__init__(test_registry, *args) + super().__init__(example_registry, *args) -@LayerImplementation(RuntimeMode.DEFAULT) -class Layer(DefaultImplementationMixin, LayerBase): - """Designate the LayerBase as the Default Mode""" +class ExampleComposed(DefaultImplementationMixin, ExampleBase): + """Compose the default implementation""" pass -@LayerImplementation(TEST_MODE) -class CustomLayerImplementation(CustomImplementationMixin, LayerBase): +# create the decorated default implementation +Example = ExampleImplementation(RuntimeMode.DEFAULT)(ExampleComposed) + + +@ExampleImplementation(TEST_MODE) +class CustomLayerImplementation(CustomImplementationMixin, ExampleBase): @classmethod def can_clone(cls, recipe: LayerRecipe) -> bool: # assume this test class can only clone layers with 'vals' lower than 100 @@ -55,69 +59,74 @@ def do_something(self): return f"{self.s}: {self.val} - made by {self.class_name()}" def class_name(self) -> str: - return self.__class__.__name__ + return "CustomClass" @pytest.fixture(scope="function", autouse=True) def clean_environment(): - test_registry.clear() + example_registry.clear() bitorch.mode = RuntimeMode.DEFAULT yield None - test_registry.clear() + example_registry.clear() bitorch.mode = RuntimeMode.DEFAULT def test_recipe(): - s1 = Layer("Hello World", val=21) - s2 = Layer("Hello World", 21) + s1 = Example("Hello World", val=21) + s2 = Example("Hello World", 21) - s1_recipe = test_registry.get_recipe_for(s1) + s1_recipe = example_registry.get_recipe_for(s1) assert s1_recipe.args[0] == "Hello World" assert s1_recipe.kwargs["val"] == 21 - s2_recipe = test_registry.get_recipe_for(s2) + s2_recipe = example_registry.get_recipe_for(s2) assert s2_recipe.args[0] == "Hello World" assert s2_recipe.args[1] == 21 def test_default_impl(): - s = Layer("Hello World", val=21) + s = Example("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "Layer" - assert isinstance(s, Layer.class_) + assert s.class_name() == "BaseClass" + assert isinstance(s, Example.class_) assert isinstance(s, LayerContainer) def test_train_impl(): bitorch.mode = TEST_MODE - s = Layer("Hello World", val=21) + s = Example("Hello World", val=21) assert s.val == 21 - assert s.class_name() == "CustomLayerImplementation" + assert s.class_name() == "CustomClass" assert isinstance(s, CustomLayerImplementation) assert isinstance(s, LayerContainer) def test_raw_impl(): bitorch.mode = RuntimeMode.RAW - s = Layer("Hello World", val=21) - assert s.val == 21 - assert s.class_name() == "Layer" - assert isinstance(s, Layer.class_) - assert not isinstance(s, LayerContainer) + layer = Example("Hello World", val=21) + assert layer.val == 21 + assert layer.class_name() == "BaseClass" + assert isinstance(layer, Example.class_) + assert not isinstance(layer, LayerContainer) + + content = pickle.dumps(layer) + + layer_loaded = pickle.loads(content) + assert layer_loaded.val == 21 @pytest.mark.parametrize("val, is_supported", [(150, False), (50, True)]) def test_clone(val, is_supported): - s = Layer("Hello World", val=val) - s_recipe = test_registry.get_recipe_for(s) + s = Example("Hello World", val=val) + s_recipe = example_registry.get_recipe_for(s) if is_supported: - replacement = test_registry.get_replacement(TEST_MODE, s_recipe) + replacement = example_registry.get_replacement(TEST_MODE, s_recipe) assert isinstance(replacement, CustomLayerImplementation) # type: ignore else: with pytest.raises(RuntimeError) as e_info: - _ = test_registry.get_replacement(TEST_MODE, s_recipe) + _ = example_registry.get_replacement(TEST_MODE, s_recipe) error_message = str(e_info.value) assert e_info.typename == "RuntimeError" - expected_key_strings = ["Layer", "implementation", str(TEST_MODE), "val", "100"] + expected_key_strings = ["Example", "implementation", str(TEST_MODE), "val", "100"] for key in expected_key_strings: assert key in error_message diff --git a/tests/layers/test_switchable_layer.py b/tests/layers/test_switchable_layer.py index 5ccc966..14e275c 100644 --- a/tests/layers/test_switchable_layer.py +++ b/tests/layers/test_switchable_layer.py @@ -1,3 +1,5 @@ +import pickle + import pytest import torch from torch import nn From 4f327fa931cc2e4f2f00a2b906558d9dacf57ce1 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 5 Jul 2022 17:54:49 +0200 Subject: [PATCH 079/208] fix distillation model wrapper --- examples/pytorch_lightning/utils/lightning_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 8647b1a..1006db4 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -113,6 +113,5 @@ def __init__(self, *args, **kwargs) -> None: param.requires_grad = False def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor) -> torch.Tensor: - y_hat_student = self.forward(x_train) y_hat_teacher = self.teacher.forward(x_train) - return self._kd_loss(y_hat_student, y_hat_teacher) + return self._kd_loss(y_hat, y_hat_teacher) From 82967781bf533be17ce534bce6564bdc93ca758b Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 5 Jul 2022 18:10:09 +0200 Subject: [PATCH 080/208] add test for pickling and fix decorator problems --- bitorch/layers/qconv1d.py | 4 +-- bitorch/layers/qconv2d.py | 4 +-- bitorch/layers/qconv3d.py | 4 +-- bitorch/layers/qlinear.py | 5 +-- .../utils/lightning_model.py | 6 ++-- examples/pytorch_lightning/utils/teachers.py | 2 ++ ...r_impl.py => test_layer_implementation.py} | 34 +++++++++++-------- 7 files changed, 34 insertions(+), 25 deletions(-) rename tests/layers/{test_layer_impl.py => test_layer_implementation.py} (81%) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 140ba8d..62a96aa 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -1,6 +1,6 @@ """Module containing the quantized 1d convolution layer""" -from typing import Any, Union +from typing import Any, Type, Union from torch import Tensor from torch.nn import Conv1d, init @@ -122,4 +122,4 @@ class QConv1dComposed(DefaultImplementationMixin, QConv1dBase): pass -QConv1d = QConv1dImplementation(RuntimeMode.DEFAULT)(QConv1dComposed) +QConv1d: Type[QConv1dComposed] = QConv1dImplementation(RuntimeMode.DEFAULT)(QConv1dComposed) # type: ignore diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 578721e..ccf1898 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -1,6 +1,6 @@ """Module containing the quantized 2d convolution layer""" -from typing import Union, Any +from typing import Any, Type, Union from torch import Tensor from torch.nn import Conv2d, init @@ -116,4 +116,4 @@ class QConv2dComposed(DefaultImplementationMixin, QConv2dBase): pass -QConv2d = QConv2dImplementation(RuntimeMode.DEFAULT)(QConv2dComposed) +QConv2d: Type[QConv2dComposed] = QConv2dImplementation(RuntimeMode.DEFAULT)(QConv2dComposed) # type: ignore diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index 88329d2..c3b6fc8 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -1,6 +1,6 @@ """Module containing the quantized 3d convolution layer""" -from typing import Union, Any +from typing import Any, Type, Union from torch import Tensor from torch.nn import Conv3d, init @@ -116,4 +116,4 @@ class QConv3dComposed(DefaultImplementationMixin, QConv3dBase): pass -QConv3d = QConv3dImplementation(RuntimeMode.DEFAULT)(QConv3dComposed) +QConv3d: Type[QConv3dComposed] = QConv3dImplementation(RuntimeMode.DEFAULT)(QConv3dComposed) # type: ignore diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 8903253..45c1fa9 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -1,5 +1,6 @@ """Module containing the quantized linear layer""" -from typing import Union, Dict, Any + +from typing import Any, Type, Union, Dict import torch from torch.nn import Linear @@ -88,4 +89,4 @@ class QLinearComposed(DefaultImplementationMixin, QLinearBase): pass -QLinear = QLinearImplementation(RuntimeMode.DEFAULT)(QLinearComposed) +QLinear: Type[QLinearComposed] = QLinearImplementation(RuntimeMode.DEFAULT)(QLinearComposed) # type: ignore diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 1006db4..4d6c0a6 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -1,6 +1,6 @@ import logging from argparse import Namespace -from typing import Union +from typing import Union, Any import torch from pytorch_lightning import LightningModule @@ -54,7 +54,7 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore self.log("loss/train", loss, on_step=True, on_epoch=False) return loss - def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor): + def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor) -> torch.Tensor: return self.loss_function(y_hat, y_train) def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: ignore @@ -104,7 +104,7 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i class DistillationModelWrapper(ModelWrapper): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._kd_loss = DistributionLoss() self.teacher = get_teacher(self.hparams.teacher) diff --git a/examples/pytorch_lightning/utils/teachers.py b/examples/pytorch_lightning/utils/teachers.py index a13cb07..d90bb34 100644 --- a/examples/pytorch_lightning/utils/teachers.py +++ b/examples/pytorch_lightning/utils/teachers.py @@ -29,8 +29,10 @@ def resnet50() -> nn.Module: def available_teachers() -> List[str]: + """Return a list of all available model names (pre-trained on ImageNet).""" return list(_teachers().keys()) def get_teacher(teacher_name: str) -> nn.Module: + """Return a model pretrained on ImageNet for a given model name.""" return _teachers()[teacher_name]() diff --git a/tests/layers/test_layer_impl.py b/tests/layers/test_layer_implementation.py similarity index 81% rename from tests/layers/test_layer_impl.py rename to tests/layers/test_layer_implementation.py index d34eb4b..09b9b1d 100644 --- a/tests/layers/test_layer_impl.py +++ b/tests/layers/test_layer_implementation.py @@ -85,20 +85,26 @@ def test_recipe(): def test_default_impl(): - s = Example("Hello World", val=21) - assert s.val == 21 - assert s.class_name() == "BaseClass" - assert isinstance(s, Example.class_) - assert isinstance(s, LayerContainer) + layer = Example("Hello World", val=21) + assert layer.val == 21 + assert layer.class_name() == "BaseClass" + assert isinstance(layer, Example.class_) + assert isinstance(layer, LayerContainer) + + # TODO: pickling is currently only possible in RAW mode + # content = pickle.dumps(layer) + # + # layer_loaded = pickle.loads(content) + # assert layer_loaded.val == 21 def test_train_impl(): bitorch.mode = TEST_MODE - s = Example("Hello World", val=21) - assert s.val == 21 - assert s.class_name() == "CustomClass" - assert isinstance(s, CustomLayerImplementation) - assert isinstance(s, LayerContainer) + layer = Example("Hello World", val=21) + assert layer.val == 21 + assert layer.class_name() == "CustomClass" + assert isinstance(layer, CustomLayerImplementation) + assert isinstance(layer, LayerContainer) def test_raw_impl(): @@ -117,14 +123,14 @@ def test_raw_impl(): @pytest.mark.parametrize("val, is_supported", [(150, False), (50, True)]) def test_clone(val, is_supported): - s = Example("Hello World", val=val) - s_recipe = example_registry.get_recipe_for(s) + layer = Example("Hello World", val=val) + recipe = example_registry.get_recipe_for(layer) if is_supported: - replacement = example_registry.get_replacement(TEST_MODE, s_recipe) + replacement = example_registry.get_replacement(TEST_MODE, recipe) assert isinstance(replacement, CustomLayerImplementation) # type: ignore else: with pytest.raises(RuntimeError) as e_info: - _ = example_registry.get_replacement(TEST_MODE, s_recipe) + _ = example_registry.get_replacement(TEST_MODE, recipe) error_message = str(e_info.value) assert e_info.typename == "RuntimeError" expected_key_strings = ["Example", "implementation", str(TEST_MODE), "val", "100"] From 0cb521d3c7de2f4cadc5933fe0f849521829ee27 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 6 Jul 2022 12:20:25 +0200 Subject: [PATCH 081/208] actually save the best model, not the worst --- examples/pytorch_lightning/image_classification.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 0cd8de5..d0a012f 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -46,6 +46,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: """ configure_logging(logger, args.log_file, args.log_level, args.log_stdout) + # switch to RAW bitorch mode for distributed data parallel training bitorch.mode = RuntimeMode.RAW apply_args_to_configuration(args) @@ -74,7 +75,10 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: args.checkpoint_dir, save_last=True, save_top_k=args.checkpoint_keep_count, + every_n_epochs=1, monitor="metrics/test-top1-accuracy", + mode="max", + filename="{epoch:03d}", ) ) From 28027ff2516e72fdb228fb44558ca4a439cbde08 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 6 Jul 2022 12:32:43 +0200 Subject: [PATCH 082/208] upload best model to wandb --- .../pytorch_lightning/image_classification.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index d0a012f..5c97866 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -69,18 +69,18 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) # type: ignore ) callbacks: List[Any] = [] + check_point_callback = None if args.checkpoint_dir is not None: - callbacks.append( - ModelCheckpoint( - args.checkpoint_dir, - save_last=True, - save_top_k=args.checkpoint_keep_count, - every_n_epochs=1, - monitor="metrics/test-top1-accuracy", - mode="max", - filename="{epoch:03d}", - ) + check_point_callback = ModelCheckpoint( + args.checkpoint_dir, + save_last=True, + save_top_k=args.checkpoint_keep_count, + every_n_epochs=1, + monitor="metrics/test-top1-accuracy", + mode="max", + filename="{epoch:03d}", ) + callbacks.append(check_point_callback) # providing our own progress bar disables the default progress bar (not needed to disable later on) cmd_logger = CommandLineLogger(args.log_interval) @@ -178,6 +178,10 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ckpt_path=args.checkpoint_load if not args.pretrained else None, ) + if args.wandb_log and check_point_callback is not None: + wandb.run.summary["best-model-score"] = check_point_callback.best_model_score + wandb.save(check_point_callback.best_model_path) + if __name__ == "__main__": parser, model_parser = create_argparser() From ef8d3166c1d19b9dce3b16fed6a1e47636fd9597 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 6 Jul 2022 17:43:13 +0200 Subject: [PATCH 083/208] implement progressive sign --- bitorch/config.py | 10 +- bitorch/quantizations/__init__.py | 2 + bitorch/quantizations/base.py | 20 +--- bitorch/quantizations/config.py | 3 + bitorch/quantizations/progressive_sign.py | 113 ++++++++++++++++++ .../pytorch_lightning/image_classification.py | 9 +- examples/pytorch_lightning/utils/callbacks.py | 20 ++++ .../utils/lightning_model.py | 2 +- tests/quantizations/test_quantizations.py | 30 +++++ 9 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 bitorch/quantizations/progressive_sign.py create mode 100644 examples/pytorch_lightning/utils/callbacks.py diff --git a/bitorch/config.py b/bitorch/config.py index f23071d..d57e785 100644 --- a/bitorch/config.py +++ b/bitorch/config.py @@ -12,6 +12,8 @@ class Config: subclasses. """ + name: str + def __init__(self) -> None: """collects all attributes of class that are not the name as configurable attributes.""" configurable_attributes = [ @@ -25,11 +27,11 @@ def __init__(self) -> None: self._add_getter_setter_methods(attribute) def _add_getter_setter_methods(self, attribute: str) -> None: - def getter(self): # type: ignore - return getattr(self, attribute) + def getter(self_): # type: ignore + return getattr(self_, attribute) - def setter(self, value): # type: ignore - setattr(self, attribute, value) + def setter(self_, value): # type: ignore + setattr(self_, attribute, value) setattr(self, f"get_{attribute}", getter) setattr(self, f"set_{attribute}", setter) diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 535bcae..cdae11f 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -14,6 +14,7 @@ from .sign import Sign from .ste_heaviside import SteHeaviside from .swish_sign import SwishSign +from .progressive_sign import ProgressiveSign from ..util import build_lookup_dictionary __all__ = [ @@ -22,6 +23,7 @@ "InputDoReFa", "WeightDoReFa", "Identity", + "ProgressiveSign", "Sign", "SteHeaviside", "SwishSign", diff --git a/bitorch/quantizations/base.py b/bitorch/quantizations/base.py index 41e64ed..c2f8e83 100644 --- a/bitorch/quantizations/base.py +++ b/bitorch/quantizations/base.py @@ -1,12 +1,13 @@ """Quantization superclass implementation""" -import torch import typing -from torch import nn -from torch.autograd.function import Function from typing import Any from warnings import warn +import torch +from torch import nn +from torch.autograd.function import Function + class STE(Function): """Straight Through estimator for backward pass""" @@ -17,16 +18,7 @@ def forward( ctx: torch.autograd.function.BackwardCFunction, # type: ignore input_tensor: torch.Tensor, ) -> torch.Tensor: - """just fowards the unchanged input_tensor. - - Args: - ctx (Any): autograd context - input_tensor (torch.Tensor): input tensor - - Returns: - torch.Tensor: the unchanged input tensor - """ - return input_tensor + raise NotImplementedError("Forwards pass of STE should be implemented by subclass.") @staticmethod @typing.no_type_check @@ -55,7 +47,7 @@ def bitwidth(self) -> int: return self.bit_width def quantize(self, x: torch.Tensor) -> torch.Tensor: - """Quantize the input tensor. + """Apply the quantization function to the input tensor. It is recommended to use a torch.Function to also manipulate backwards behavior. See the implementations of sign or dorefa quantization functions for more examples. diff --git a/bitorch/quantizations/config.py b/bitorch/quantizations/config.py index ad1f6a5..d15c3e6 100644 --- a/bitorch/quantizations/config.py +++ b/bitorch/quantizations/config.py @@ -10,5 +10,8 @@ class QuantizationConfig(Config): # beta value for swishsign function beta = 5.0 + # scaling of progressive sign function, should be zero at the start of the training, and (close to) one at the end + progressive_sign_scale = 0.0 + config = QuantizationConfig() diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py new file mode 100644 index 0000000..ee477b8 --- /dev/null +++ b/bitorch/quantizations/progressive_sign.py @@ -0,0 +1,113 @@ +"""Progressive Sign Function""" +import typing +from typing import Any, Callable, Optional + +import torch +import torch.nn.functional as F + +from .base import Quantization, STE +from .config import config +from .sign import SignFunction + +EPSILON = 1e-7 + + +class ProgressiveSignFunctionTrain(STE): + @staticmethod + @typing.no_type_check + def forward( + ctx: torch.autograd.function.BackwardCFunction, # type: ignore + input_tensor: torch.Tensor, + temperature: float, + ) -> torch.Tensor: + """Binarize the input tensor using the sign function + + Args: + ctx (Any): autograd context + input_tensor (torch.Tensor): input tensor + temperature: the temperature of the incline + + Returns: + torch.Tensor: the sign tensor + """ + # avoid division by zero with EPSILON + return F.hardtanh(input_tensor / max(1.0 - temperature, EPSILON)) + + @staticmethod + @typing.no_type_check + def backward(ctx: Any, output_gradient: torch.Tensor) -> torch.Tensor: + return output_gradient, None # type: ignore + + +class ProgressiveSign(Quantization): + """ + Module for applying a progressive sign function with STE during training. + + During validation a regular sign function is used. + This can lead to a significant accuracy difference during the first epochs. + With a temperature of one this function is basically equal to a regular sign function. + """ + + name = "progressive_sign" + bit_width = 1 + + scale: float + global_scaling: bool + + def __init__( + self, + use_global_scaling: bool = True, + initial_scale: Optional[float] = None, + custom_transform: Optional[Callable[[float], float]] = None, + ) -> None: + """ + Initialize the progressive sign module (can be used for progressive weight binarization). + + If `use_global_scaling` is set to False, the scale of this module must be set manually. + Otherwise, the value can be set for all progressive sign modules in the config. + + Args: + use_global_scaling: whether to use the global scaling variable stored in the config + initial_scale: if not using global scaling you can set an initial scale + custom_transform: to use a custom transform function from scale to temperature, add it here + """ + super().__init__() + if initial_scale is not None and use_global_scaling: + raise RuntimeWarning( + "An initial scale was set on ProgressiveSign, but this has not effect, " + "since use_global_scaling is True." + ) + self.global_scaling = use_global_scaling + self.scale = initial_scale or config.progressive_sign_scale + self.custom_transform = custom_transform + + @property + def current_scale(self) -> float: + if self.global_scaling: + return config.progressive_sign_scale + return self.scale + + @staticmethod + def default_transform(x: float) -> float: + return 1 - (5 ** (-3 * x)) + + def transform(self, x: float) -> float: + """Transform x for a steady temperature increase, higher at the beginning, and much less at the end.""" + if self.custom_transform is not None: + return self.custom_transform(x) + return self.default_transform(x) + + def quantize(self, x: torch.Tensor) -> torch.Tensor: + """Forwards the tensor through the sign function. + + Args: + x (torch.Tensor): tensor to be forwarded. + + Returns: + torch.Tensor: sign of tensor x + """ + if self.training: + temperature = self.transform(self.current_scale) + return ProgressiveSignFunctionTrain.apply(x, temperature) + else: + return SignFunction.apply(x) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 5c97866..a842d7f 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,5 +1,7 @@ import os +from examples.pytorch_lightning.utils.callbacks import ProgressiveSignScalerCallback + if os.environ.get("REMOTE_PYCHARM_DEBUG_SESSION", False): import pydevd_pycharm @@ -63,7 +65,6 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: loggers.append( WandbLogger( project=args.wandb_project, - log_model=True, name=args.wandb_experiment, save_dir=str(output_dir), ) # type: ignore @@ -87,6 +88,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: callbacks.append(cmd_logger) configure_logging(cmd_logger.logger, args.log_file, args.log_level, args.log_stdout) + # add scaling callback for progressive sign (not be needed for all models, but should not slow down training) + callbacks.append(ProgressiveSignScalerCallback()) + if len(loggers) > 0: lr_monitor = LearningRateMonitor(logging_interval="step") callbacks.append(lr_monitor) @@ -178,8 +182,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ckpt_path=args.checkpoint_load if not args.pretrained else None, ) + # backup best model to wandb if args.wandb_log and check_point_callback is not None: - wandb.run.summary["best-model-score"] = check_point_callback.best_model_score + wandb.run.summary["best-model-score"] = check_point_callback.best_model_score # type: ignore wandb.save(check_point_callback.best_model_path) diff --git a/examples/pytorch_lightning/utils/callbacks.py b/examples/pytorch_lightning/utils/callbacks.py new file mode 100644 index 0000000..9bda972 --- /dev/null +++ b/examples/pytorch_lightning/utils/callbacks.py @@ -0,0 +1,20 @@ +import pytorch_lightning as pl + +from bitorch.quantizations import ProgressiveSign +from bitorch.quantizations.config import config as quantization_config + + +class ProgressiveSignScalerCallback(pl.callbacks.Callback): + """Callback that updates the scale of progressive sign functions based on current epoch.""" + + def on_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + scale = trainer.current_epoch / trainer.max_epochs + quantization_config.progressive_sign_scale = scale + for logger in trainer.loggers: + logger.log_metrics( + { + "_progressive_sign_scale": scale, + "_progressive_sign_temperature": ProgressiveSign.default_transform(scale), + }, + step=trainer.fit_loop.epoch_loop._batches_that_stepped, + ) diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 4d6c0a6..ba6e320 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -107,8 +107,8 @@ class DistillationModelWrapper(ModelWrapper): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._kd_loss = DistributionLoss() + logging.info(f"Training with Knowledge Distillation, loading teacher {self.hparams.teacher}.") self.teacher = get_teacher(self.hparams.teacher) - # self.teacher.eval() for param in self.teacher.parameters(): param.requires_grad = False diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index da3d667..502655a 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -8,6 +8,7 @@ ApproxSign, SteHeaviside, SwishSign, + ProgressiveSign, quantization_from_name, ) @@ -19,6 +20,34 @@ [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), + ( + ProgressiveSign(), + 1, + [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], + [-1.0, -1.0, -0.3, 0.0, 0.3, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + ProgressiveSign(use_global_scaling=False, initial_scale=0.2), + 1, + [-1.5, -1.0, -0.3, -0.1, 0.0, 0.1, 0.3, 1.0, 1.5], + [-1.0, -1.0, -0.788, -0.2627, 0.0, 0.2627, 0.788, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + ProgressiveSign(use_global_scaling=False, initial_scale=0.5), + 1, + [-1.5, -1.0, -0.3, -0.1, -0.05, 0.0, 0.05, 0.1, 0.3, 1.0, 1.5], + [-1.0, -1.0, -1.0, -1.0, -0.559, 0.0, 0.559, 1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), + ( + ProgressiveSign(use_global_scaling=False, initial_scale=0.2, custom_transform=lambda x: x), + 1, + [-1.5, -1.0, -0.3, -0.1, 0.0, 0.1, 0.3, 1.0, 1.5], + [-1.0, -1.0, -0.375, -0.125, 0.0, 0.125, 0.375, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + ), ( ApproxSign(), 1, @@ -84,6 +113,7 @@ def test_quantizations( assert isinstance(quantization, quantization_from_name(quantization.name)) y = quantization(x) + assert len(y) == len(x_exp) assert torch.allclose(y, x_exp, atol=0.001) assert quantization.bit_width == bits with pytest.deprecated_call(): From 733ee2fbb7e780d5b551f6b8f90f530cc6039332 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 6 Jul 2022 17:56:54 +0200 Subject: [PATCH 084/208] add to changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c41da..7ae9b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - support for integration of bitorch's inference engine for the following layers - QLinear - QConv +- New quantization function: [Progressive Sign](bitorch/quantizations/progressive_sign.py) +- New features in PyTorch Lightning example: + - Training with Knowledge Distillation + - Improved Logging + - Callback to update Progressive Sign modules ### Changed - requirements changed: + - code now depends on torch 1.12.x and torchvision 0.13.x - requirements for examples are now stored at their respective folders - optional requirements now install everything needed to run all examples - code is now formatted with the black code formatter From 662f6352bdb2fed965bf12ed7798325cd6d61ec7 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 09:02:53 +0200 Subject: [PATCH 085/208] cleaner version of wandb model saving and add dev run option for fast debugging --- .../pytorch_lightning/image_classification.py | 38 +++++++++---------- .../pytorch_lightning/utils/arg_parser.py | 5 +++ .../pytorch_lightning/utils/wandb_logger.py | 17 +++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 examples/pytorch_lightning/utils/wandb_logger.py diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index a842d7f..f09167d 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -1,7 +1,5 @@ import os -from examples.pytorch_lightning.utils.callbacks import ProgressiveSignScalerCallback - if os.environ.get("REMOTE_PYCHARM_DEBUG_SESSION", False): import pydevd_pycharm @@ -22,7 +20,7 @@ import wandb from pytorch_lightning import Trainer from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor -from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase, WandbLogger +from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase from torch.utils.data import DataLoader import bitorch @@ -31,7 +29,9 @@ from bitorch.datasets.base import Augmentation from bitorch.models import model_from_name from bitorch.quantizations import Quantization +from examples.pytorch_lightning.utils.callbacks import ProgressiveSignScalerCallback from examples.pytorch_lightning.utils.log import CommandLineLogger +from examples.pytorch_lightning.utils.wandb_logger import CustomWandbLogger from utils.arg_parser import create_argparser from utils.lightning_model import ModelWrapper, DistillationModelWrapper from utils.utils import configure_logging @@ -63,25 +63,26 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: loggers.append(CSVLogger(str(output_dir), name="csv")) if args.wandb_log: loggers.append( - WandbLogger( + CustomWandbLogger( project=args.wandb_project, name=args.wandb_experiment, save_dir=str(output_dir), + log_model=True, ) # type: ignore ) callbacks: List[Any] = [] - check_point_callback = None if args.checkpoint_dir is not None: - check_point_callback = ModelCheckpoint( - args.checkpoint_dir, - save_last=True, - save_top_k=args.checkpoint_keep_count, - every_n_epochs=1, - monitor="metrics/test-top1-accuracy", - mode="max", - filename="{epoch:03d}", + callbacks.append( + ModelCheckpoint( + args.checkpoint_dir, + save_last=True, + save_top_k=args.checkpoint_keep_count, + every_n_epochs=1, + monitor="metrics/test-top1-accuracy", + mode="max", + filename="{epoch:03d}", + ) ) - callbacks.append(check_point_callback) # providing our own progress bar disables the default progress bar (not needed to disable later on) cmd_logger = CommandLineLogger(args.log_interval) @@ -127,8 +128,12 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger=loggers if len(loggers) > 0 else None, # type: ignore callbacks=callbacks, # type: ignore log_every_n_steps=args.log_interval, + limit_train_batches=0.01 if args.dev_run else None, + limit_val_batches=0.01 if args.dev_run else None, ) augmentation_level = Augmentation.from_string(args.augmentation) + if args.dev_run: + logger.info(f"This run only uses 1 % of training and validation data (--dev-run)!") logger.info(f"model: {args.model}") logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") @@ -182,11 +187,6 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ckpt_path=args.checkpoint_load if not args.pretrained else None, ) - # backup best model to wandb - if args.wandb_log and check_point_callback is not None: - wandb.run.summary["best-model-score"] = check_point_callback.best_model_score # type: ignore - wandb.save(check_point_callback.best_model_path) - if __name__ == "__main__": parser, model_parser = create_argparser() diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 84455f0..af9875b 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -292,6 +292,11 @@ def add_regular_args(parser: ArgumentParser) -> None: action="store_true", help="explicitly use the cpu. overwrites gpu settings", ) + parser.add_argument( + "--dev-run", + action="store_true", + help="use only 1% of training/validation data for testing purposes", + ) def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: diff --git a/examples/pytorch_lightning/utils/wandb_logger.py b/examples/pytorch_lightning/utils/wandb_logger.py new file mode 100644 index 0000000..fe06dd2 --- /dev/null +++ b/examples/pytorch_lightning/utils/wandb_logger.py @@ -0,0 +1,17 @@ +from pytorch_lightning.loggers import WandbLogger +from pytorch_lightning.utilities import rank_zero_only + + +class CustomWandbLogger(WandbLogger): + """ + Customized Wandb Logger with the following changes: + + - the last model is not uploaded to wandb at the end of the training + """ + + @rank_zero_only + def finalize(self, status: str) -> None: + if self._checkpoint_callback: + # disable saving the last model to wandb + self._checkpoint_callback.last_model_path = "" + super().finalize(status) From 6503020699d7a7ac6cc1d26467890817ee133a2a Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 09:12:41 +0200 Subject: [PATCH 086/208] automatically add some tags --- .../pytorch_lightning/image_classification.py | 1 + examples/pytorch_lightning/utils/wandb_logger.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index f09167d..323f839 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -64,6 +64,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: if args.wandb_log: loggers.append( CustomWandbLogger( + args, project=args.wandb_project, name=args.wandb_experiment, save_dir=str(output_dir), diff --git a/examples/pytorch_lightning/utils/wandb_logger.py b/examples/pytorch_lightning/utils/wandb_logger.py index fe06dd2..7c202d2 100644 --- a/examples/pytorch_lightning/utils/wandb_logger.py +++ b/examples/pytorch_lightning/utils/wandb_logger.py @@ -1,3 +1,6 @@ +from argparse import Namespace +from typing import Any + from pytorch_lightning.loggers import WandbLogger from pytorch_lightning.utilities import rank_zero_only @@ -7,8 +10,21 @@ class CustomWandbLogger(WandbLogger): Customized Wandb Logger with the following changes: - the last model is not uploaded to wandb at the end of the training + - automatically adds some tags based on the command line arguments """ + def __init__(self, script_args: Namespace, *args: Any, **kwargs: Any) -> None: + wandb_tags = [script_args.model, script_args.dataset] + if script_args.dev_run: + wandb_tags.append("dev-run") + if script_args.teacher: + wandb_tags.append("kd") + if "tags" in kwargs: + kwargs["tags"].extend(wandb_tags) + else: + kwargs["tags"] = wandb_tags + super().__init__(*args, **kwargs) + @rank_zero_only def finalize(self, status: str) -> None: if self._checkpoint_callback: From 1e5c98a605826da5e2b411aefd047c067390ffca Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 10:22:09 +0200 Subject: [PATCH 087/208] fix flake issue --- examples/pytorch_lightning/image_classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 323f839..b406c2c 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -134,7 +134,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) augmentation_level = Augmentation.from_string(args.augmentation) if args.dev_run: - logger.info(f"This run only uses 1 % of training and validation data (--dev-run)!") + logger.info("This run only uses 1 % of training and validation data (--dev-run)!") logger.info(f"model: {args.model}") logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") From 6b22b8660efbe3bdcf2ea665fb213d3f0afce979 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 11:01:23 +0200 Subject: [PATCH 088/208] add prefix to tags --- examples/pytorch_lightning/utils/wandb_logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/pytorch_lightning/utils/wandb_logger.py b/examples/pytorch_lightning/utils/wandb_logger.py index 7c202d2..ef0550a 100644 --- a/examples/pytorch_lightning/utils/wandb_logger.py +++ b/examples/pytorch_lightning/utils/wandb_logger.py @@ -14,7 +14,8 @@ class CustomWandbLogger(WandbLogger): """ def __init__(self, script_args: Namespace, *args: Any, **kwargs: Any) -> None: - wandb_tags = [script_args.model, script_args.dataset] + kv_tags = ["model", "dataset"] + wandb_tags = [f"{k}:{getattr(script_args, k, 'unknown')}" for k in kv_tags] if script_args.dev_run: wandb_tags.append("dev-run") if script_args.teacher: From bf17a7192d23a2de2410e4ad0ff18286753143b6 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 12:01:34 +0200 Subject: [PATCH 089/208] add minimum versions --- examples/pytorch_lightning/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pytorch_lightning/requirements.txt b/examples/pytorch_lightning/requirements.txt index 66db890..da0934b 100644 --- a/examples/pytorch_lightning/requirements.txt +++ b/examples/pytorch_lightning/requirements.txt @@ -1,5 +1,5 @@ bitorch fvbitcore -pytorch_lightning +pytorch_lightning~=1.6.0 sklearn -wandb +wandb~=0.12.0 From 763204acbad674158e63fea1232faeb10c5d9264 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 15:26:18 +0200 Subject: [PATCH 090/208] trying to improve documentation --- bitorch/layers/qconv1d.py | 9 +++++++-- bitorch/layers/qconv2d.py | 9 +++++++-- bitorch/layers/qconv3d.py | 9 +++++++-- bitorch/layers/qlinear.py | 9 +++++++-- docs/source/_templates/class.rst | 8 +------- docs/source/_templates/module.rst | 4 +++- docs/source/conf.py | 1 + 7 files changed, 33 insertions(+), 16 deletions(-) diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 62a96aa..48dcaf2 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -112,7 +112,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -class QConv1dComposed(DefaultImplementationMixin, QConv1dBase): +class _QConv1dComposed(DefaultImplementationMixin, QConv1dBase): """ This class defines the default implementation of a QConv1d layer (which is actually implemented by QConv1dBase). @@ -122,4 +122,9 @@ class QConv1dComposed(DefaultImplementationMixin, QConv1dBase): pass -QConv1d: Type[QConv1dComposed] = QConv1dImplementation(RuntimeMode.DEFAULT)(QConv1dComposed) # type: ignore +QConv1d: Type[_QConv1dComposed] = QConv1dImplementation(RuntimeMode.DEFAULT)(_QConv1dComposed) # type: ignore +""" +This class provides the current implementation of a QConv1d layer (which is actually implemented by :class:`QConv1dBase`). + +To implement a custom QConv1d implementation use :class:`QConv1dBase` as a super class instead. +""" diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index ccf1898..3b57d0d 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -106,7 +106,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -class QConv2dComposed(DefaultImplementationMixin, QConv2dBase): +class _QConv2dComposed(DefaultImplementationMixin, QConv2dBase): """ This class defines the default implementation of a QConv2d layer (which is actually implemented by QConv2dBase). @@ -116,4 +116,9 @@ class QConv2dComposed(DefaultImplementationMixin, QConv2dBase): pass -QConv2d: Type[QConv2dComposed] = QConv2dImplementation(RuntimeMode.DEFAULT)(QConv2dComposed) # type: ignore +QConv2d: Type[_QConv2dComposed] = QConv2dImplementation(RuntimeMode.DEFAULT)(_QConv2dComposed) # type: ignore +""" +This class provides the current implementation of a QConv2d layer (which is actually implemented by :class:`QConv2dBase`). + +To implement a custom QConv2d implementation use :class:`QConv2dBase` as a super class instead. +""" diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index c3b6fc8..e976b02 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -106,7 +106,7 @@ def forward(self, input_tensor: Tensor) -> Tensor: return super().forward(self.activation(input_tensor)) -class QConv3dComposed(DefaultImplementationMixin, QConv3dBase): +class _QConv3dComposed(DefaultImplementationMixin, QConv3dBase): """ This class defines the default implementation of a QConv3d layer (which is actually implemented by QConv3dBase). @@ -116,4 +116,9 @@ class QConv3dComposed(DefaultImplementationMixin, QConv3dBase): pass -QConv3d: Type[QConv3dComposed] = QConv3dImplementation(RuntimeMode.DEFAULT)(QConv3dComposed) # type: ignore +QConv3d: Type[_QConv3dComposed] = QConv3dImplementation(RuntimeMode.DEFAULT)(_QConv3dComposed) # type: ignore +""" +This class provides the current implementation of a QConv3d layer (which is actually implemented by :class:`QConv3dBase`). + +To implement a custom QConv3d implementation use :class:`QConv3dBase` as a super class instead. +""" diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 45c1fa9..968c49a 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -79,7 +79,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return linear(self.activation(x), self.weight_quantization(self.weight), self.bias) -class QLinearComposed(DefaultImplementationMixin, QLinearBase): +class _QLinearComposed(DefaultImplementationMixin, QLinearBase): """ This class defines the default implementation of a QLinear layer (which is actually implemented by QLinearBase). @@ -89,4 +89,9 @@ class QLinearComposed(DefaultImplementationMixin, QLinearBase): pass -QLinear: Type[QLinearComposed] = QLinearImplementation(RuntimeMode.DEFAULT)(QLinearComposed) # type: ignore +QLinear: Type[_QLinearComposed] = QLinearImplementation(RuntimeMode.DEFAULT)(_QLinearComposed) # type: ignore +""" +This class provides the current implementation of a QLinear layer (which is actually implemented by :class:`QLinearBase`). + +To implement a custom QLinear implementation use :class:`QLinearBase` as a super class instead. +""" diff --git a/docs/source/_templates/class.rst b/docs/source/_templates/class.rst index 9f31d95..9ba91a8 100644 --- a/docs/source/_templates/class.rst +++ b/docs/source/_templates/class.rst @@ -5,7 +5,7 @@ .. autoclass:: {{ objname }} :members: :no-inherited-members: - :special-members: __call__, __add__, __mul__, forward, __init__ + :special-members: __call__, __add__, __mul__, forward {% block methods %} {% if methods %} @@ -14,12 +14,6 @@ .. autosummary:: :nosignatures: {% for item in methods %} - - {%- if item.startswith('__init__') %} - {%- if item not in inherited_members %} - ~{{ name }}.{{ item }} - {%- endif -%} - {%- endif -%} {%- if not item.startswith('_') %} {%- if item not in inherited_members %} ~{{ name }}.{{ item }} diff --git a/docs/source/_templates/module.rst b/docs/source/_templates/module.rst index b3c490f..10f5e26 100644 --- a/docs/source/_templates/module.rst +++ b/docs/source/_templates/module.rst @@ -54,6 +54,8 @@ {% block modules %} {% if modules %} +.. rubric:: {{ _('Modules') }} + .. autosummary:: :toctree: :template: module.rst @@ -62,4 +64,4 @@ {{ item }} {%- endfor %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/docs/source/conf.py b/docs/source/conf.py index 611c17d..b332d24 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -52,6 +52,7 @@ templates_path = ["_templates"] autosummary_generate = True +autoclass_content = "init" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From d961b9fcc230c0ebd7f131668885a4aa46e48c6b Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 15:31:44 +0200 Subject: [PATCH 091/208] back to config on github --- docs/source/_templates/class.rst | 8 +++++++- docs/source/_templates/module.rst | 2 -- docs/source/conf.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/source/_templates/class.rst b/docs/source/_templates/class.rst index 9ba91a8..39b2c4f 100644 --- a/docs/source/_templates/class.rst +++ b/docs/source/_templates/class.rst @@ -5,7 +5,7 @@ .. autoclass:: {{ objname }} :members: :no-inherited-members: - :special-members: __call__, __add__, __mul__, forward + :special-members: __call__, __add__, __mul__, forward, __init__ {% block methods %} {% if methods %} @@ -14,6 +14,12 @@ .. autosummary:: :nosignatures: {% for item in methods %} + + {%- if item.startswith('__init__') %} + {%- if item not in inherited_members %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endif -%} {%- if not item.startswith('_') %} {%- if item not in inherited_members %} ~{{ name }}.{{ item }} diff --git a/docs/source/_templates/module.rst b/docs/source/_templates/module.rst index 10f5e26..881b4cd 100644 --- a/docs/source/_templates/module.rst +++ b/docs/source/_templates/module.rst @@ -54,8 +54,6 @@ {% block modules %} {% if modules %} -.. rubric:: {{ _('Modules') }} - .. autosummary:: :toctree: :template: module.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index b332d24..4db63d8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -46,13 +46,13 @@ ] # Generate type hints -autodoc_typehints = "description" +# autodoc_typehints = "description" # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] autosummary_generate = True -autoclass_content = "init" +# autoclass_content = "init" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From 78bd1a31657f4009c23ea3aea78920c4c25c41c5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 15:48:15 +0200 Subject: [PATCH 092/208] add rubric for modules --- docs/source/_templates/module.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/_templates/module.rst b/docs/source/_templates/module.rst index 881b4cd..10f5e26 100644 --- a/docs/source/_templates/module.rst +++ b/docs/source/_templates/module.rst @@ -54,6 +54,8 @@ {% block modules %} {% if modules %} +.. rubric:: {{ _('Modules') }} + .. autosummary:: :toctree: :template: module.rst From 531c750353dc7a0e8e403ca015888c0e5c25a441 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 7 Jul 2022 16:53:11 +0200 Subject: [PATCH 093/208] bump dev version --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 13668bb..8a3ba5a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.0.dev0 +0.3.0.dev1 From d5dc3c3f7d9f61bc7daa4a6767cfacd70f97c6dd Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 15 Jul 2022 14:47:41 +0200 Subject: [PATCH 094/208] added dlrm train code --- bitorch/models/__init__.py | 2 + bitorch/models/dlrm.py | 345 ++-------- examples/dlrm/.gitignore | 1 + examples/dlrm/README.md | 60 ++ examples/dlrm/__init__.py | 1 + examples/dlrm/criteo.py | 75 ++ .../dlrm/facebook_dataloading/__init__.py | 0 .../dlrm/facebook_dataloading/data_utils.py | 647 ++++++++++++++++++ .../facebook_dataloading/dataloading_fb.py | 398 +++++++++++ examples/dlrm/requirements.txt | 6 + examples/dlrm/train_dlrm.py | 230 +++++++ examples/dlrm/utils/__init__.py | 1 + examples/dlrm/utils/arg_parser.py | 305 +++++++++ examples/dlrm/utils/lightning_model.py | 120 ++++ examples/dlrm/utils/log.py | 177 +++++ examples/dlrm/utils/unused_args.py | 73 ++ examples/dlrm/utils/utils.py | 105 +++ 17 files changed, 2241 insertions(+), 305 deletions(-) create mode 100644 examples/dlrm/.gitignore create mode 100644 examples/dlrm/README.md create mode 100644 examples/dlrm/__init__.py create mode 100644 examples/dlrm/criteo.py create mode 100644 examples/dlrm/facebook_dataloading/__init__.py create mode 100644 examples/dlrm/facebook_dataloading/data_utils.py create mode 100644 examples/dlrm/facebook_dataloading/dataloading_fb.py create mode 100644 examples/dlrm/requirements.txt create mode 100644 examples/dlrm/train_dlrm.py create mode 100644 examples/dlrm/utils/__init__.py create mode 100644 examples/dlrm/utils/arg_parser.py create mode 100644 examples/dlrm/utils/lightning_model.py create mode 100644 examples/dlrm/utils/log.py create mode 100644 examples/dlrm/utils/unused_args.py create mode 100644 examples/dlrm/utils/utils.py diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index dd2e1d0..8adf638 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -25,6 +25,7 @@ ResnetE18, ResnetE34, ) +from .dlrm import DLRM from ..util import build_lookup_dictionary __all__ = [ @@ -42,6 +43,7 @@ "ResnetE", "ResnetE18", "ResnetE34", + "DLRM", ] diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index f97167d..974d9ac 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -1,32 +1,29 @@ from argparse import ArgumentParser -import math -from pytorch_lightning import LightningModule from enum import Enum -from typing import List, Optional, Tuple, Union +from typing import List, Tuple, Union import logging import torch from torch.nn import ( Linear, + Sequential, + PReLU, Sigmoid, EmbeddingBag, ModuleList, - ParameterList, - Parameter, BatchNorm1d, ) -import torch.nn.functional as F import numpy as np -import sklearn.metrics as metrics +from bitorch.datasets.base import BasicDataset from bitorch.layers import QLinear +from bitorch.models.base import Model # from .utils import create_loss_function, create_optimizer, create_activation_function, parse_layer_sizes, str2bool from bitorch.layers.qembedding import QEmbeddingBag -class Weighted_Pooling_Type(Enum): - NONE = 0 - FIXED = 1 - LEARNED = 2 +def parse_layer_sizes(layer_sizes_str: str) -> List[int]: + layer_sizes_str = layer_sizes_str.replace('[', '').replace(']', '') + return [int(size) for size in layer_sizes_str.split(",")] class Interaction_Operation_Type(Enum): @@ -35,65 +32,28 @@ class Interaction_Operation_Type(Enum): SUM = "sum" -class MLP(torch.nn.Module): - """Mlp class for DLRM mlps. this mainly is used to properly pass shortcut data""" - name = "MLP" - - def __init__(self, layers: ModuleList, name: str = "MLP", - shortcut_out_index: Union[int, None] = None, shortcut_in_index: Union[int, None] = None): - super().__init__() - self.shortcut_out_index = shortcut_out_index - self.shortcut_in_index = shortcut_in_index - self.layers = layers - self.name = name - logging.info(f"shortcut_out_index {shortcut_out_index}") - logging.info(f"shortcut_in_index {shortcut_in_index}") - - def forward(self, X, shortcut_value=None): - out = None - Y = X - for index, layer in enumerate(self.layers): - if index == self.shortcut_in_index: - Y = Y + shortcut_value - Y = layer(Y) - if index == self.shortcut_out_index: - out = Y - return Y, out - - def __str__(self) -> str: - out = f"{self.name}\n" - out += "=" * 10 - out += "\n".join(self.layers) - return out - - def create_mlp( layer_sizes: List[int], - quantized: bool = False) -> MLP: + quantized: bool = False) -> Sequential: """creates a mlp module Args: layer_sizes (List[int]): linear layer unit sizes - sigmoid_layer_idx (int): the layer number to use a sigmoid activation function. + for size in enumerate(layer_sizes_str.split(",")): + parsed_layer_sizes.append(int(size))oid activation function. all other layers will have relu activation. """ input_size = layer_sizes[0] - this_shortcut_out_index = None - this_shortcut_in_index = None mlp_layers = [] - for idx, layer_size in enumerate(layer_sizes[1:]): - output_size = layer_size - mlp_layers.append(BatchNorm1d(input_size)) + for layer_size in layer_sizes[1:]: + output_size = layer_size + print(input_size, output_size) + mlp_layers.append(BatchNorm1d(input_size)) mlp_layers.append( QLinear(input_size, output_size, bias=False) if quantized else - Linear(input_size, output_size, bias=True) + Linear(input_size, output_size, bias=True) ) - - if idx == shortcut_out_index and this_shortcut_out_index is None: - this_shortcut_out_index = len(mlp_layers) - if idx == shortcut_in_index and this_shortcut_in_index is None: - this_shortcut_in_index = len(mlp_layers) mean = 0.0 # std_dev = np.sqrt(variance) std_dev = np.sqrt(2 / (output_size + input_size)) # np.sqrt(1 / m) # np.sqrt(1 / n) mlp_weight = np.random.normal(mean, std_dev, size=(output_size, input_size)).astype(np.float32) @@ -104,44 +64,30 @@ def create_mlp( if mlp_layers[-1].bias is not None: mlp_layers[-1].bias.data = torch.tensor(mlp_bias, requires_grad=True) - if batch_norm_before_relu: - mlp_layers.append(BatchNorm1d(output_size)) - mlp_layers.append( - Sigmoid() if idx == sigmoid_layer_idx else ( - create_activation_function( - activation_function, bitwidth))) + mlp_layers.append(BatchNorm1d(output_size)) + mlp_layers.append(PReLU()) input_size = output_size - if sigmoid_layer_idx == -1: - mlp_layers[-1] = Sigmoid() - return MLP( - ModuleList(mlp_layers), - name=name, - shortcut_out_index=this_shortcut_out_index, - shortcut_in_index=this_shortcut_in_index) + return Sequential(*mlp_layers) def create_embeddings( embedding_dimension: int, layer_sizes: List[int], - weighted_pooling: Weighted_Pooling_Type, - binary_embedding: bool, - add_linear_to_binary_embeddings: bool, + quantized: bool, sparse=False) -> Tuple[ModuleList, List[Union[None, torch.Tensor]]]: """creates the embedding layers for each category.""" if sparse: logging.info("USING SPARSE EMBEDDINGS") embedding_layers = ModuleList() - weighted_layers = [] for layer_size in layer_sizes: logging.info( f"creating embedding layer with {layer_size} * {embedding_dimension} = {layer_size * embedding_dimension} params...") - if binary_embedding: + if quantized: embedding_layers.append(QEmbeddingBag( layer_size, embedding_dim=embedding_dimension, mode="mean", sparse=sparse, - use_linear_layer=add_linear_to_binary_embeddings )) else: embedding_layers.append(EmbeddingBag(layer_size, embedding_dimension, mode="sum", sparse=sparse)) @@ -149,53 +95,30 @@ def create_embeddings( low=-np.sqrt(1 / layer_size), high=np.sqrt(1 / layer_size), size=(layer_size, embedding_dimension) ).astype(np.float32) embedding_layers[-1].weight.data = torch.tensor(embedding_weights, requires_grad=True) - weighted_layers.append( - torch.ones(layer_size, dtype=torch.float32) - if not weighted_pooling == Weighted_Pooling_Type.NONE.value - else None) - return embedding_layers, weighted_layers + return embedding_layers -class DLRM(Module): +class DLRM(Model): + name = "dlrm" total_size = 1.0 inference_speed = 1.0 validation_results = [] def __init__( self, + dataset: BasicDataset, dense_feature_size: int, - weighted_pooling: Weighted_Pooling_Type, embedding_dimension: int, embedding_layer_sizes: List[int], bottom_mlp_layer_sizes: List[int], - bottom_sigmoid_layer_idx: int, top_mlp_layer_sizes: List[int], - top_full_precision_layers: List[int], - bottom_full_precision_layers: List[int], - top_sigmoid_layer_idx: int, interaction_operation: Interaction_Operation_Type, binary_bottom_mlp: bool, binary_top_mlp: bool, - batch_norm_before_relu: bool, - batch_norm_before_sign: bool, binary_embedding: bool, - add_linear_to_binary_embeddings: bool, - activation_function: str, - bitwidth: int, - optimizer: str, - momentum: float, - lr: float, - lr_scheduler: str, - lr_factor: float, - lr_steps: List[int], - epochs: int, - loss: str, - loss_weights: Union[None, List[float]], - threshold: float, - shortcut: str = "none", **kwargs) -> None: - super().__init__() + super().__init__(dataset) self.interaction_operation = interaction_operation self.embedding_layers = create_embeddings( embedding_dimension, @@ -203,8 +126,8 @@ def __init__( binary_embedding, ) - bottom_mlp_layer_sizes, self.sc_out_index = parse_layer_sizes(bottom_mlp_layer_sizes) - top_mlp_layer_sizes, self.sc_in_index = parse_layer_sizes(top_mlp_layer_sizes) + bottom_mlp_layer_sizes = parse_layer_sizes(bottom_mlp_layer_sizes) + top_mlp_layer_sizes = parse_layer_sizes(top_mlp_layer_sizes) # computing the correct bottom and top mlp layer sizes taking into account # feature dimensions and feature interaction output shapes @@ -215,7 +138,8 @@ def __init__( elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: top_mlp_layer_sizes = [ embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] - + print(bottom_mlp_layer_sizes) + print(top_mlp_layer_sizes) self.bottom_mlp = create_mlp( bottom_mlp_layer_sizes, quantized=binary_bottom_mlp, @@ -224,27 +148,15 @@ def __init__( top_mlp_layer_sizes, quantized=binary_top_mlp, ) - self.loss_function, self.loss_weights = create_loss_function(loss, loss_weights) + self.top_mlp[-1] = Sigmoid() @staticmethod def add_argparse_arguments(parent_parser: ArgumentParser): parser = parent_parser.add_argument_group("DLRM Model") - parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64, 1]", + parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64]", help="layer sizes of the bottom mlp") - parser.add_argument("--top-mlp-layer-sizes", type=str, default="[512, 256]", - # parser.add_argument("--top-mlp-layer-sizes", type=int, nargs="*", - # default=[48, 512, 256, 1], + parser.add_argument("--top-mlp-layer-sizes", type=str, default="[512, 256, 1]", help="layer sizes of the top mlp") - parser.add_argument( - "--bottom-sigmoid-layer-idx", - type=int, - default=None, - help="index of the sigmoid activation function in the bottom mlp (default is disabled, -1 is last)") - parser.add_argument( - "--top-sigmoid-layer-idx", - type=int, - default=-1, - help="index of the sigmoid activation function in the top mlp (None is disabled, -1 is last)") parser.add_argument("--embedding-dimension", type=int, default=16, help="number of embedding dimensions") parser.add_argument( @@ -253,63 +165,14 @@ def add_argparse_arguments(parent_parser: ArgumentParser): Interaction_Operation_Type.CONCAT.value, Interaction_Operation_Type.PRODUCT.value], default=Interaction_Operation_Type.CONCAT.value) - parser.add_argument( - "--weighted-pooling", - choices=list(Weighted_Pooling_Type), - default=Weighted_Pooling_Type.NONE.value) - parser.add_argument("--loss", type=str, default="mse", - help="name of loss function") - parser.add_argument('--loss-weights', nargs="*", default=None, - help='list loss weights. this is only used by bce loss') - parser.add_argument("--lr-scheduler", type=str, - choices=["cosine", "step", "exponential"], - help="name of the lr scheduler to use. default to none") - parser.add_argument("--lr", type=float, default=0.1, - help="initial learning rate (default: 0.1)") - parser.add_argument( - '--lr-factor', - default=0.1, - type=float, - help='learning rate decay ratio. this is used only by the step and exponential lr scheduler') - parser.add_argument('--lr-steps', nargs="*", default=[30, 60, 90], - help='list of learning rate decay epochs as list. this is used only by the step scheduler') - parser.add_argument('--momentum', type=float, default=0.9, - help='momentum value for optimizer, default is 0.9. only used for sgd optimizer') - parser.add_argument('--optimizer', type=str, default="sgd", choices=["adam", "sgd", "sparse_adam", "radam"], - help='the optimizer to use. default is adam.') - parser.add_argument("--threshold", type=float, default=0.5, - help="threshold which is used to binarize predictions") - parser.add_argument("--lr-num-warmup-steps", type=int, default=0, - help="number of warmups steps of the lr scheduler") - parser.add_argument("--lr-decay-start-step", type=int, default=0, help="number of steps until the lr decays") - parser.add_argument("--lr-num-decay-steps", type=int, default=0, help="number of steps how long the lr decays") - parser.add_argument("--full-embeddings", action="store_false", help="Disable sparse embeddings") + parser.add_argument("--dense-embeddings", action="store_false", help="Disable sparse embeddings") - parser.add_argument("--add-linear-to-binary-embeddings", action="store", type=str2bool, default=False, - help="whether to add an linear layer to binary embeddings") - parser.add_argument("--binary-embedding", action="store", type=str2bool, default=False, + parser.add_argument("--binary-embedding", action="store_true", default=False, help="toggles use of binary embeddings in model.") - parser.add_argument("--binary-top-mlp", action="store", type=str2bool, default=False, + parser.add_argument("--binary-top-mlp", action="store_true", default=False, help="toggles use of binary top mlp in model.") - parser.add_argument("--binary-bottom-mlp", action="store", type=str2bool, default=False, + parser.add_argument("--binary-bottom-mlp", action="store_true", default=False, help="toggles use of binary bottom mlp in model.") - parser.add_argument("--batch-norm-before-relu", action="store", type=str2bool, default=False, - help="toggles use of binary bottom mlp in model.") - parser.add_argument("--batch-norm-before-sign", action="store", type=str2bool, default=False, - help="toggles use of binary bottom mlp in model.") - parser.add_argument( - "--activation-function", - choices=[ - "relu", - "prelu", - "pact"], - default="relu", - type=str, - help="select activation function") - parser.add_argument('--top-full-precision-layers', nargs="*", default=[], - help='list of learning rate decay epochs as list. this is used only by the step scheduler') - parser.add_argument('--bottom-full-precision-layers', nargs="*", default=[], - help='list of learning rate decay epochs as list. this is used only by the step scheduler') return parent_parser def forward_embeddings(self, categorical_values_i: torch.Tensor, @@ -317,14 +180,9 @@ def forward_embeddings(self, categorical_values_i: torch.Tensor, """forwards the preprocessed data through the embedding layers.""" embedding_outputs = [] for index, embedding_layer in enumerate(self.embedding_layers): - weight_pooling_layer = self.weight_pooling_layers[index] index_group = categorical_values_i[index] offset_group = categorical_values_o[index] - if weight_pooling_layer is not None: - per_sample_weights = weight_pooling_layer.gather(0, index_group) - else: - per_sample_weights = None - embedding_outputs.append(embedding_layer(index_group, offset_group, per_sample_weights=per_sample_weights)) + embedding_outputs.append(embedding_layer(index_group, offset_group)) return embedding_outputs def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[torch.Tensor]): @@ -345,136 +203,13 @@ def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[ return result def forward(self, dense_values, categorical_values): - mlp_output, shortcout_output = self.bottom_mlp(dense_values) + mlp_output = self.bottom_mlp(dense_values) embedding_outputs = self.forward_embeddings(*categorical_values) feature_interactions = self.feature_interaction(mlp_output, embedding_outputs) - interaction_probability, _ = self.top_mlp(feature_interactions, shortcout_output) + interaction_probability = self.top_mlp(feature_interactions) # if the top mlp has multiple output values, aggregate these into one single value if len(interaction_probability.shape) > 1 and interaction_probability.shape[1] > 1: interaction_probability = torch.clamp(interaction_probability, 0, 1) interaction_probability = torch.mean(interaction_probability, dim=1) return interaction_probability - - def training_step(self, batch, batch_idx): - dense_values, categorical_values_i, categorical_values_o, y = batch - if isinstance(categorical_values_i, list): - for el in categorical_values_i: - el.to(self.device) - else: - categorical_values_i = categorical_values_i.to(self.device) - if isinstance(categorical_values_o, list): - for el in categorical_values_o: - el.to(self.device) - else: - categorical_values_o = categorical_values_o.to(self.device) - dense_values.to(self.device) - y_hat = self(dense_values, (categorical_values_i, categorical_values_o)) - - loss = self.loss_function(torch.squeeze(y_hat), torch.squeeze(y)) - self.log_dict({"loss": loss}) - return loss - - def validation_step_end(self, *args, **kwargs): - """calculate all them metrics and log via wandb/tensorboard""" - - y = torch.cat(list(map(lambda x: x["y"], self.validation_results))) - y_hat = torch.cat(list(map(lambda x: x["y_hat"], self.validation_results))) - loss = self.loss_function(y, y_hat) - rmse = torch.sqrt(F.mse_loss(y_hat, y)).item() - y_array = np.array(y.cpu()) - y_hat_array = np.array(y_hat.cpu()) >= self.hparams.threshold - balanced_accuracy = metrics.balanced_accuracy_score(y_array, y_hat_array) - accuracy = metrics.accuracy_score(y_array, y_hat_array) - f1 = metrics.f1_score(y_array, y_hat_array) - roc_auc = metrics.roc_auc_score(y_array, y_hat.cpu()) - precision = metrics.precision_score(y_array, y_hat_array) - recall = metrics.recall_score(y_array, y_hat_array) - self.log_dict({ - "val_los": loss, - "val_rmse": rmse, - "roc_auc": roc_auc, - "precision": precision, - "recall": recall, - "balanced accuracy": balanced_accuracy, - "accuracy": accuracy, - "f1 score": f1, - "model size": self.total_size, - "inference speed": self.inference_speed, - "weighted accuracy": accuracy / (self.total_size * self.inference_speed), - "weighted speed accuracy": accuracy / (self.total_size * self.inference_speed), - "log2 weighted speed accuracy": accuracy / math.log(self.total_size * self.inference_speed, 2.0) - }, prog_bar=True) - return super().validation_step_end(*args, **kwargs) - - def on_validation_start(self) -> None: - self.validation_results = [] - return super().on_validation_start() - - def validation_step(self, batch, batch_idx): - dense_values, categorical_values_i, categorical_values_o, y = batch - dense_values = dense_values.to(self.device) - if isinstance(categorical_values_i, list): - for el in categorical_values_i: - el.to(self.device) - else: - categorical_values_i = categorical_values_i.to(self.device) - if isinstance(categorical_values_o, list): - for el in categorical_values_o: - el.to(self.device) - else: - categorical_values_o = categorical_values_o.to(self.device) - y_hat = torch.squeeze(self(dense_values, (categorical_values_i, categorical_values_o))) - y = torch.squeeze(y) - y_hat = torch.squeeze(y_hat) - self.validation_results.append({"y": y, "y_hat": y_hat}) - - def configure_optimizers(self): - configuration = {} - optimizer = create_optimizer(self.hparams.optimizer, self, self.hparams.lr, self.hparams.momentum) - configuration["optimizer"] = optimizer - lr_scheduler = LRPolicyScheduler( - optimizer, - self.hparams.lr_num_warmup_steps, - self.hparams.lr_decay_start_step, - self.hparams.lr_num_decay_steps, - ) - if lr_scheduler is not None: - configuration["lr_scheduler"] = lr_scheduler_config = { - # REQUIRED: The scheduler instance - "scheduler": lr_scheduler, - # The unit of the scheduler's step size, could also be 'step'. - # 'epoch' updates the scheduler on epoch end whereas 'step' - # updates it after a optimizer update. - "interval": "step", - # How many epochs/steps should pass between calls to - # `scheduler.step()`. 1 corresponds to updating the learning - # rate after every epoch/step. - "frequency": 1, - # Metric to to monitor for schedulers like `ReduceLROnPlateau` - "monitor": "balanced accuracy", - # If set to `True`, will enforce that the value specified 'monitor' - # is available when the scheduler is updated, thus stopping - # training if not found. If set to `False`, it will only produce a warning - "strict": True, - # If using the `LearningRateMonitor` callback to monitor the - # learning rate progress, this keyword can be used to specify - # a custom logged name - "name": "LRScheduler", - } - return configuration - - def transform_categorical(self, categorical: List[torch.Tensor]): - categorical_batch_indices = [[torch.tensor(*np.where(category_batch[batch_num].numpy())) - for batch_num in range(category_batch.size(0))] for category_batch in categorical] - offsets = [torch.tensor([len(index_array) for index_array in category_batch]) - for category_batch in categorical_batch_indices] - for category_index, category_sizes in enumerate(offsets): - offsets[category_index] = offsets[category_index].roll(1) - offsets[category_index][0] = 0 - for size_index in range(1, len(category_sizes)): - offsets[category_index][size_index] += offsets[category_index][size_index - 1] - - concatenated_categorical_batch_indices = [ - torch.cat(category_batch) for category_batch in categorical_batch_indices] - return list(zip(concatenated_categorical_batch_indices, offsets)) diff --git a/examples/dlrm/.gitignore b/examples/dlrm/.gitignore new file mode 100644 index 0000000..333c1e9 --- /dev/null +++ b/examples/dlrm/.gitignore @@ -0,0 +1 @@ +logs/ diff --git a/examples/dlrm/README.md b/examples/dlrm/README.md new file mode 100644 index 0000000..9b4d4cd --- /dev/null +++ b/examples/dlrm/README.md @@ -0,0 +1,60 @@ +# Pytorch Lightning Example Script + +To give an example on how to use bitorch for your own projects `image_classification.py` trains one of the +models implemented in `bitorch` on an image classification dataset. + +First the requirements for this example need to be installed +(unless the optional dependencies of BITorch were already installed): +```bash +pip install -r requirements.txt +``` + +Below you can find an example call of the script: +```bash +python3 image_classification.py --optimizer adam --lr 0.001 --lr-scheduler cosine --max_epochs 2 --dataset imagenet --model resnet18v1 --batch-size 128 --accelerator gpu --num-workers 16 --gpus 3 +``` + +## Arguments + +To find an exhaustive overview over the parameters to configure the `image_classification.py` script, call `python image_classification.py --help`. +The list below gives a brief overview over some selected arguments. + +### general training args + +- `--optimizer` sets the optimizer. Choose from `adam, sgd` and `radam`. +- `--lr-scheduler` sets the learning rate scheduler. Choose from `cosine, step` and `exponential` +- `--lr` sets the used learning rate. +- `--max-epochs` sets the number of epochs to train. +- `--max-steps` sets the number of training steps to perform. +- `--batch-size` sets batchsize to use +- `--gpus n` specify number of gpus to use. if `n` not specified, all available gpus will be used. +- `--cpu` force training on cpu. + +### logging args + +- `--log-file` specifies the file to log into +- `--log-stdout` toggles if the log output should also go to stdout +- `--tensorboar` toggles logging to tensorboard +- `--wandb` toggles logging to wandb. You need to specify a WANDB_API_TOKEN variable in your environment to use this. [details](https://docs.wandb.ai/guides/track/public-api-guide#authentication) +- `--result-file` specifies path to a result file which will contain the evaluation metrics in csv format. +- `--checkpoint-dir` path to where checkpoints shall be stored +- `--checkpoint-load` path to checkpoint to load from + +### model args + +- `--model` specify name of model you want to train. Choose from `lenet,resnet,resnet152v1,resnet152v2,resnet18v1,resnet18v2,resnet34v1,resnet34v2,resnet50v1,resnet50v2,resnete,resnete18` or `resnete34` + +Each model can have specific arguments. Check them by calling `python image_classification.py --help`. + +### dataset args + +- `--datset` name of dataset to train on. Chose from `mnist, cifar10, cifar100` and `imagenet` +- `--download` toggles if dataset if not present at `--dataset-dir` should be downloaded. Only available for `mnist` and `cifar10`. +- `--dataset-dir` path to dataset. +- `--num-worker` sets number of workers for dataloading + +### quantization args + +- `--input-quantization` chooses the default input quantization method. +- `--weight-quantization` chooses the default weight quantization method. +- `--gradient-cancellation-threshold` sets the default gradient cancellation threshold diff --git a/examples/dlrm/__init__.py b/examples/dlrm/__init__.py new file mode 100644 index 0000000..aa5b8b1 --- /dev/null +++ b/examples/dlrm/__init__.py @@ -0,0 +1 @@ +"""This package contains an example for image classification with BITorch.""" diff --git a/examples/dlrm/criteo.py b/examples/dlrm/criteo.py new file mode 100644 index 0000000..fdc429b --- /dev/null +++ b/examples/dlrm/criteo.py @@ -0,0 +1,75 @@ +import gc +from torch.utils.data import Dataset +import logging +import os +import numpy as np +from bitorch.datasets.base import BasicDataset +from facebook_dataloading.dataloading_fb import CriteoDataset + + +class SplitCriteoDataset(Dataset): + """Dataset to get items from a dataset for each split. Useful if dataset creation takes a lot of time and can be done exactly once.""" + def __init__(self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0): + self.dataset = dataset + + # split_index = int(train_split_fraction * len(dataset)) + # self.indices = list(range(len(dataset))) + # self.indices = self.indices[:split_index] if split == "train" else self.indices[split_index:] + self.indices = self.dataset.train_indices if split == "train" else self.dataset.test_indices + + dataset_size = int(len(self.indices) * (1.0 - ignore_size)) + self.indices = np.random.choice(self.indices, size=dataset_size, replace=False) + + def __getitem__(self, idx): + return self.dataset[self.indices[idx]] + + def __len__(self): + return len(self.indices) + + +class Criteo(BasicDataset): + name = "criteo" + + num_train_samples = 60000 + num_val_samples = 10000 + dataset_url = 'http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz' + + def get_dataset(self, download: bool = True) -> Dataset: + try: + self.download_path = self.root_directory / "criteo.tar.gz" + self.path = self.root_directory / "train.txt" + self.path.parent.mkdir(parents=True, exist_ok=True) + if download and not self.download_path.exists(): + logging.info("DOWNLOADING CRITEO DATASET") + result = os.system(f"wget {self.dataset_url} -O {str(self.root_directory / 'criteo.tar.gz')}") + if result != 0: + raise Exception("Download failed") + logging.info("FINISHED DOWNLOAD") + if not (self.root_directory / 'train.txt').exists(): + logging.info("EXTRACTING CRITEO DATASET") + result = os.system(f"tar -xf {str(self.root_directory / 'criteo.tar.gz')} -C {self.root_directory}") + if result != 0: + raise Exception("Extraction failed") + logging.info("FINISHED EXTRACTION") + except Exception as e: + logging.error( + f"Cannot get criteo dataset: {e}. You need download " + "the dataset manually under the following link: " + "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz " + f"and extract it to the following path: {str(self.root_directory.resolve())}. " + "alternatively you can try downloading it automatically by using the --download flag" + ) + dataset = CriteoDataset( + dataset="kaggle", + max_ind_range=-1, + sub_sample_rate=0.0, + randomize="total", + # split="train" if self.is_train else "test", + raw_path=str(self.root_directory / "train.txt"), + pro_data=str(self.root_directory / "preprocessed.npz"), + memory_map=False, + dataset_multiprocessing=False, + store_all_indices=True, + ) + gc.collect() + return dataset diff --git a/examples/dlrm/facebook_dataloading/__init__.py b/examples/dlrm/facebook_dataloading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py new file mode 100644 index 0000000..84556f5 --- /dev/null +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -0,0 +1,647 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +# Description: generate inputs and targets for the DLRM benchmark +# +# Utility function(s) to download and pre-process public data sets +# - Criteo Kaggle Display Advertising Challenge Dataset +# https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset +# - Criteo Terabyte Dataset +# https://labs.criteo.com/2013/12/download-terabyte-click-logs +# +# After downloading dataset, run: +# getCriteoAdData( +# datafile="", +# o_filename=kaggleAdDisplayChallenge_processed.npz, +# max_ind_range=-1, +# sub_sample_rate=0.0, +# days=7, +# data_split='train', +# randomize='total', +# criteo_kaggle=True, +# memory_map=False +# ) +# getCriteoAdData( +# datafile="", +# o_filename=terabyte_processed.npz, +# max_ind_range=-1, +# sub_sample_rate=0.0, +# days=24, +# data_split='train', +# randomize='total', +# criteo_kaggle=False, +# memory_map=False +# ) + +from __future__ import absolute_import, division, print_function, unicode_literals + +from pathlib import Path +import sys +# import os +import logging +from os import path +from multiprocessing import Process, Manager +# import io +# from io import StringIO +# import collections as coll + +import numpy as np + + +def processCriteoAdData(d_path, d_file, npzfile, i, convertDicts, pre_comp_counts): + # Process Kaggle Display Advertising Challenge or Terabyte Dataset + # by converting unicode strings in X_cat to integers and + # converting negative integer values in X_int. + # + # Loads data in the form "{kaggle|terabyte}_day_i.npz" where i is the day. + # + # Inputs: + # d_path (str): path for {kaggle|terabyte}_day_i.npz files + # i (int): splits in the dataset (typically 0 to 7 or 0 to 24) + + # process data if not all files exist + filename_i = npzfile + "_{0}_processed.npz".format(i) + + if path.exists(filename_i): + logging.debug("Using existing " + filename_i, end="\n") + else: + logging.debug("Not existing " + filename_i) + with np.load(npzfile + "_{0}.npz".format(i)) as data: + # categorical features + # Approach 2a: using pre-computed dictionaries + X_cat_t = np.zeros(data["X_cat_t"].shape) + for j in range(26): + for k, x in enumerate(data["X_cat_t"][j, :]): + X_cat_t[j, k] = convertDicts[j][x] + # continuous features + X_int = data["X_int"] + X_int[X_int < 0] = 0 + # targets + y = data["y"] + + np.savez_compressed( + filename_i, + # X_cat = X_cat, + X_cat=np.transpose(X_cat_t), # transpose of the data + X_int=X_int, + y=y, + ) + logging.debug("Processed " + filename_i, end="\n") + # sanity check (applicable only if counts have been pre-computed & are re-computed) + # for j in range(26): + # if pre_comp_counts[j] != counts[j]: + # sys.exit("ERROR: Sanity check on counts has failed") + # logging.debug("\nSanity check on counts passed") + + return + + +def concatCriteoAdData( + d_path, + d_file, + npzfile, + trafile, + days, + data_split, + randomize, + total_per_file, + total_count, + memory_map, + o_filename +): + # Concatenates different days and saves the result. + # + # Inputs: + # days (int): total number of days in the dataset (typically 7 or 24) + # d_path (str): path for {kaggle|terabyte}_day_i.npz files + # o_filename (str): output file name + # + # Output: + # o_file (str): output file path + + if memory_map: + # dataset break up per fea + # tar_fea = 1 # single target + den_fea = 13 # 13 dense features + spa_fea = 26 # 26 sparse features + # tad_fea = tar_fea + den_fea + # tot_fea = tad_fea + spa_fea + # create offset per file + offset_per_file = np.array([0] + [x for x in total_per_file]) + for i in range(days): + offset_per_file[i + 1] += offset_per_file[i] + + # Approach 4: Fisher-Yates-Rao (FYR) shuffle algorithm + # 1st pass of FYR shuffle + # check if data already exists + recreate_flag = False + for j in range(days): + filename_j_y = npzfile + "_{0}_intermediate_y.npy".format(j) + filename_j_d = npzfile + "_{0}_intermediate_d.npy".format(j) + filename_j_s = npzfile + "_{0}_intermediate_s.npy".format(j) + if ( + path.exists(filename_j_y) + and path.exists(filename_j_d) + and path.exists(filename_j_s) + ): + logging.debug( + "Using existing\n" + + filename_j_y + "\n" + + filename_j_d + "\n" + + filename_j_s + ) + else: + recreate_flag = True + # reorder across buckets using sampling + if recreate_flag: + # init intermediate files (.npy appended automatically) + for j in range(days): + filename_j_y = npzfile + "_{0}_intermediate_y".format(j) + filename_j_d = npzfile + "_{0}_intermediate_d".format(j) + filename_j_s = npzfile + "_{0}_intermediate_s".format(j) + np.save(filename_j_y, np.zeros((total_per_file[j]))) + np.save(filename_j_d, np.zeros((total_per_file[j], den_fea))) + np.save(filename_j_s, np.zeros((total_per_file[j], spa_fea))) + # start processing files + total_counter = [0] * days + for i in range(days): + filename_i = npzfile + "_{0}_processed.npz".format(i) + with np.load(filename_i) as data: + X_cat = data["X_cat"] + X_int = data["X_int"] + y = data["y"] + size = len(y) + # sanity check + if total_per_file[i] != size: + sys.exit("ERROR: sanity check on number of samples failed") + # debug prints + logging.debug("Reordering (1st pass) " + filename_i) + + # create buckets using sampling of random ints + # from (discrete) uniform distribution + buckets = [] + for _j in range(days): + buckets.append([]) + counter = [0] * days + days_to_sample = days if data_split == "none" else days - 1 + if randomize == "total": + rand_u = np.random.randint(low=0, high=days_to_sample, size=size) + for k in range(size): + # sample and make sure elements per buckets do not overflow + if data_split == "none" or i < days - 1: + # choose bucket + p = rand_u[k] + # retry of the bucket is full + while total_counter[p] + counter[p] >= total_per_file[p]: + p = np.random.randint(low=0, high=days_to_sample) + else: # preserve the last day/bucket if needed + p = i + buckets[p].append(k) + counter[p] += 1 + else: # randomize is day or none + for k in range(size): + # do not sample, preserve the data in this bucket + p = i + buckets[p].append(k) + counter[p] += 1 + + # sanity check + if np.sum(counter) != size: + sys.exit("ERROR: sanity check on number of samples failed") + # debug prints + # logging.debug(counter) + # logging.debug(str(np.sum(counter)) + " = " + str(size)) + # logging.debug([len(x) for x in buckets]) + # logging.debug(total_counter) + + # partially feel the buckets + for j in range(days): + filename_j_y = npzfile + "_{0}_intermediate_y.npy".format(j) + filename_j_d = npzfile + "_{0}_intermediate_d.npy".format(j) + filename_j_s = npzfile + "_{0}_intermediate_s.npy".format(j) + start = total_counter[j] + end = total_counter[j] + counter[j] + # target buckets + fj_y = np.load(filename_j_y, mmap_mode='r+') + # logging.debug("start=" + str(start) + " end=" + str(end) + # + " end - start=" + str(end - start) + " " + # + str(fj_y[start:end].shape) + " " + # + str(len(buckets[j]))) + fj_y[start:end] = y[buckets[j]] + del fj_y + # dense buckets + fj_d = np.load(filename_j_d, mmap_mode='r+') + # logging.debug("start=" + str(start) + " end=" + str(end) + # + " end - start=" + str(end - start) + " " + # + str(fj_d[start:end, :].shape) + " " + # + str(len(buckets[j]))) + fj_d[start:end, :] = X_int[buckets[j], :] + del fj_d + # sparse buckets + fj_s = np.load(filename_j_s, mmap_mode='r+') + # logging.debug("start=" + str(start) + " end=" + str(end) + # + " end - start=" + str(end - start) + " " + # + str(fj_s[start:end, :].shape) + " " + # + str(len(buckets[j]))) + fj_s[start:end, :] = X_cat[buckets[j], :] + del fj_s + # update counters for next step + total_counter[j] += counter[j] + + # 2nd pass of FYR shuffle + # check if data already exists + for j in range(days): + filename_j = npzfile + "_{0}_reordered.npz".format(j) + if path.exists(filename_j): + logging.debug("Using existing " + filename_j) + else: + recreate_flag = True + # reorder within buckets + if recreate_flag: + for j in range(days): + filename_j_y = npzfile + "_{0}_intermediate_y.npy".format(j) + filename_j_d = npzfile + "_{0}_intermediate_d.npy".format(j) + filename_j_s = npzfile + "_{0}_intermediate_s.npy".format(j) + fj_y = np.load(filename_j_y) + fj_d = np.load(filename_j_d) + fj_s = np.load(filename_j_s) + + indices = range(total_per_file[j]) + if randomize == "day" or randomize == "total": + if data_split == "none" or j < days - 1: + indices = np.random.permutation(range(total_per_file[j])) + + filename_r = npzfile + "_{0}_reordered.npz".format(j) + logging.debug("Reordering (2nd pass) " + filename_r) + np.savez_compressed( + filename_r, + X_cat=fj_s[indices, :], + X_int=fj_d[indices, :], + y=fj_y[indices], + ) + + else: + logging.debug("Concatenating multiple days into %s.npz file" % str(d_path + o_filename)) + + # load and concatenate data + for i in range(days): + filename_i = npzfile + "_{0}_processed.npz".format(i) + with np.load(filename_i) as data: + if i == 0: + X_cat = data["X_cat"] + X_int = data["X_int"] + y = data["y"] + else: + X_cat = np.concatenate((X_cat, data["X_cat"])) + X_int = np.concatenate((X_int, data["X_int"])) + y = np.concatenate((y, data["y"])) + logging.debug("Loaded day:", i, "y = 1:", len(y[y == 1]), "y = 0:", len(y[y == 0])) + + with np.load(d_path + d_file + "_fea_count.npz") as data: + counts = data["counts"] + logging.debug("Loaded counts!") + + logging.debug("saving compressed dataset") + np.savez_compressed( + d_path + o_filename + ".npz", + X_cat=X_cat, + X_int=X_int, + y=y, + counts=counts, + ) + + return d_path + o_filename + ".npz" + + +def getCriteoAdData( + datafile, + o_filename, + max_ind_range=-1, + sub_sample_rate=0.0, + days=7, + data_split='train', + randomize='total', + criteo_kaggle=True, + memory_map=False, + dataset_multiprocessing=False, +): + # Passes through entire dataset and defines dictionaries for categorical + # features and determines the number of total categories. + # + # Inputs: + # datafile : path to downloaded raw data file + # o_filename (str): saves results under o_filename if filename is not "" + # + # Output: + # o_file (str): output file path + + # split the datafile into path and filename + lstr = datafile.split("/") + d_path = "/".join(lstr[0:-1]) + "/" + d_file = lstr[-1].split(".")[0] if criteo_kaggle else lstr[-1] + npzfile = d_path + ((d_file + "_day") if criteo_kaggle else d_file) + trafile = d_path + ((d_file + "_fea") if criteo_kaggle else "fea") + + # count number of datapoints in training set + total_file = d_path + d_file + "_day_count.npz" + if path.exists(total_file): + with np.load(total_file) as data: + total_per_file = list(data["total_per_file"]) + total_count = np.sum(total_per_file) + logging.debug("Skipping counts per file (already exist)") + else: + total_count = 0 + total_per_file = [] + if criteo_kaggle: + # WARNING: The raw data consists of a single train.txt file + # Each line in the file is a sample, consisting of 13 continuous and + # 26 categorical features (an extra space indicates that feature is + # missing and will be interpreted as 0). + if path.exists(datafile): + logging.debug("Reading data from path=%s" % (datafile)) + with open(str(datafile)) as f: + for _ in f: + total_count += 1 + total_per_file.append(total_count) + # reset total per file due to split + num_data_per_split, extras = divmod(total_count, days) + total_per_file = [num_data_per_split] * days + for j in range(extras): + total_per_file[j] += 1 + # split into days (simplifies code later on) + file_id = 0 + boundary = total_per_file[file_id] + nf = open(npzfile + "_" + str(file_id), "w") + with open(str(datafile)) as f: + for j, line in enumerate(f): + if j == boundary: + nf.close() + file_id += 1 + nf = open(npzfile + "_" + str(file_id), "w") + boundary += total_per_file[file_id] + nf.write(line) + nf.close() + else: + sys.exit("ERROR: Criteo Kaggle Display Ad Challenge Dataset path is invalid; please download from https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset") + else: + # WARNING: The raw data consist of day_0.gz,... ,day_23.gz text files + # Each line in the file is a sample, consisting of 13 continuous and + # 26 categorical features (an extra space indicates that feature is + # missing and will be interpreted as 0). + for i in range(days): + datafile_i = datafile + "_" + str(i) # + ".gz" + if path.exists(str(datafile_i)): + logging.debug("Reading data from path=%s" % (str(datafile_i))) + # file day_ + total_per_file_count = 0 + with open(str(datafile_i)) as f: + for _ in f: + total_per_file_count += 1 + total_per_file.append(total_per_file_count) + total_count += total_per_file_count + else: + sys.exit("ERROR: Criteo Terabyte Dataset path is invalid; please download from https://labs.criteo.com/2013/12/download-terabyte-click-logs") + + # process a file worth of data and reinitialize data + # note that a file main contain a single or multiple splits + def process_one_file( + datfile, + npzfile, + split, + num_data_in_split, + dataset_multiprocessing, + convertDictsDay=None, + resultDay=None + ): + if dataset_multiprocessing: + convertDicts_day = [{} for _ in range(26)] + + with open(str(datfile)) as f: + y = np.zeros(num_data_in_split, dtype="i4") # 4 byte int + X_int = np.zeros((num_data_in_split, 13), dtype="i4") # 4 byte int + X_cat = np.zeros((num_data_in_split, 26), dtype="i4") # 4 byte int + if sub_sample_rate == 0.0: + rand_u = 1.0 + else: + rand_u = np.random.uniform(low=0.0, high=1.0, size=num_data_in_split) + + i = 0 + percent = 0 + for k, line in enumerate(f): + # process a line (data point) + line = line.split('\t') + # set missing values to zero + for j in range(len(line)): + if (line[j] == '') or (line[j] == '\n'): + line[j] = '0' + # sub-sample data by dropping zero targets, if needed + target = np.int32(line[0]) + if target == 0 and \ + (rand_u if sub_sample_rate == 0.0 else rand_u[k]) < sub_sample_rate: + continue + + y[i] = target + X_int[i] = np.array(line[1:14], dtype=np.int32) + if max_ind_range > 0: + X_cat[i] = np.array( + list(map(lambda x: int(x, 16) % max_ind_range, line[14:])), + dtype=np.int32 + ) + else: + X_cat[i] = np.array( + list(map(lambda x: int(x, 16), line[14:])), + dtype=np.int32 + ) + + # count uniques + if dataset_multiprocessing: + for j in range(26): + convertDicts_day[j][X_cat[i][j]] = 1 + # debug prints + if float(i) / num_data_in_split * 100 > percent + 1: + percent = int(float(i) / num_data_in_split * 100) + logging.debug( + "Load %d/%d (%d%%) Split: %d Label True: %d Stored: %d" + % ( + i, + num_data_in_split, + percent, + split, + target, + y[i], + ), + end="\n", + ) + else: + for j in range(26): + convertDicts[j][X_cat[i][j]] = 1 + # debug prints + logging.debug( + "Load %d/%d Split: %d Label True: %d Stored: %d" + % ( + i, + num_data_in_split, + split, + target, + y[i], + ), + end="\r", + ) + i += 1 + + # store num_data_in_split samples or extras at the end of file + # count uniques + # X_cat_t = np.transpose(X_cat) + # for j in range(26): + # for x in X_cat_t[j,:]: + # convertDicts[j][x] = 1 + # store parsed + filename_s = npzfile + "_{0}.npz".format(split) + if path.exists(filename_s): + logging.debug("\nSkip existing " + filename_s) + else: + np.savez_compressed( + filename_s, + X_int=X_int[0:i, :], + # X_cat=X_cat[0:i, :], + X_cat_t=np.transpose(X_cat[0:i, :]), # transpose of the data + y=y[0:i], + ) + logging.debug("\nSaved " + npzfile + "_{0}.npz!".format(split)) + + if dataset_multiprocessing: + resultDay[split] = i + convertDictsDay[split] = convertDicts_day + return + else: + return i + + # create all splits (reuse existing files if possible) + recreate_flag = False + convertDicts = [{} for _ in range(26)] + # WARNING: to get reproducable sub-sampling results you must reset the seed below + # np.random.seed(123) + # in this case there is a single split in each day + for i in range(days): + npzfile_i = npzfile + "_{0}.npz".format(i) + npzfile_p = npzfile + "_{0}_processed.npz".format(i) + if path.exists(npzfile_i): + logging.debug("Skip existing " + npzfile_i) + elif path.exists(npzfile_p): + logging.debug("Skip existing " + npzfile_p) + else: + logging.debug("setting recreate for day", i) + recreate_flag = True + + if recreate_flag: + if dataset_multiprocessing: + resultDay = Manager().dict() + convertDictsDay = Manager().dict() + processes = [Process(target=process_one_file, + name="process_one_file:%i" % i, + args=(npzfile + "_{0}".format(i), + npzfile, + i, + total_per_file[i], + dataset_multiprocessing, + convertDictsDay, + resultDay, + ) + ) for i in range(0, days)] + for process in processes: + process.start() + for process in processes: + process.join() + for day in range(days): + total_per_file[day] = resultDay[day] + logging.debug("Constructing convertDicts Split: {}".format(day)) + convertDicts_tmp = convertDictsDay[day] + for i in range(26): + for j in convertDicts_tmp[i]: + convertDicts[i][j] = 1 + else: + for i in range(days): + total_per_file[i] = process_one_file( + npzfile + "_{0}".format(i), + npzfile, + i, + total_per_file[i], + dataset_multiprocessing, + ) + + # report and save total into a file + total_count = np.sum(total_per_file) + if not path.exists(total_file): + np.savez_compressed(total_file, total_per_file=total_per_file) + logging.debug("Total number of samples:", total_count) + logging.debug("Divided into days/splits:\n", total_per_file) + + # dictionary files + counts = np.zeros(26, dtype=np.int32) + if recreate_flag: + # create dictionaries + for j in range(26): + for i, x in enumerate(convertDicts[j]): + convertDicts[j][x] = i + dict_file_j = d_path + d_file + "_fea_dict_{0}.npz".format(j) + if not path.exists(dict_file_j): + np.savez_compressed( + dict_file_j, + unique=np.array(list(convertDicts[j]), dtype=np.int32) + ) + counts[j] = len(convertDicts[j]) + # store (uniques and) counts + count_file = d_path + d_file + "_fea_count.npz" + if not path.exists(count_file): + np.savez_compressed(count_file, counts=counts) + else: + # create dictionaries (from existing files) + for j in range(26): + with np.load(d_path + d_file + "_fea_dict_{0}.npz".format(j)) as data: + unique = data["unique"] + for i, x in enumerate(unique): + convertDicts[j][x] = i + # load (uniques and) counts + with np.load(d_path + d_file + "_fea_count.npz") as data: + counts = data["counts"] + + # process all splits + if dataset_multiprocessing: + processes = [ + Process( + target=processCriteoAdData, + name="processCriteoAdData:%i" % i, + args=(d_path, d_file, npzfile, i, convertDicts, counts,) + ) for i in range(0, days) + ] + for process in processes: + process.start() + for process in processes: + process.join() + + else: + for i in range(days): + processCriteoAdData(d_path, d_file, npzfile, i, convertDicts, counts) + + output_path = Path(d_path + o_filename + ".npz") + + if not output_path.exists(): + o_file = concatCriteoAdData( + d_path, + d_file, + npzfile, + trafile, + days, + data_split, + randomize, + total_per_file, + total_count, + memory_map, + o_filename + ) + else: + o_file = str(output_path) + + return o_file diff --git a/examples/dlrm/facebook_dataloading/dataloading_fb.py b/examples/dlrm/facebook_dataloading/dataloading_fb.py new file mode 100644 index 0000000..5618c44 --- /dev/null +++ b/examples/dlrm/facebook_dataloading/dataloading_fb.py @@ -0,0 +1,398 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +# Description: generate inputs and targets for the dlrm benchmark +# The inpts and outputs are generated according to the following three option(s) +# 1) random distribution +# 2) synthetic distribution, based on unique accesses and distances between them +# i) R. Hassan, A. Harris, N. Topham and A. Efthymiou "Synthetic Trace-Driven +# Simulation of Cache Memory", IEEE AINAM'07 +# 3) public data set +# i) Criteo Kaggle Display Advertising Challenge Dataset +# https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset +# ii) Criteo Terabyte Dataset +# https://labs.criteo.com/2013/12/download-terabyte-click-logs + + +from __future__ import absolute_import, division, print_function, unicode_literals +import gc +# others +from os import path +import sys +import logging + +from . import data_utils + +# numpy +import numpy as np + +# pytorch +import torch +from torch.utils.data import Dataset + + +# Kaggle Display Advertising Challenge Dataset +# dataset (str): name of dataset (Kaggle or Terabyte) +# randomize (str): determines randomization scheme +# "none": no randomization +# "day": randomizes each day"s data (only works if split = True) +# "total": randomizes total dataset +# split (bool) : to split into train, test, validation data-sets +class CriteoDataset(Dataset): + + def __init__( + self, + dataset, + max_ind_range, + sub_sample_rate, + randomize, + split="train", + raw_path="", + pro_data="", + memory_map=False, + dataset_multiprocessing=False, + store_all_indices=False, + ): + # dataset + # tar_fea = 1 # single target + den_fea = 13 # 13 dense features + # spa_fea = 26 # 26 sparse features + # tad_fea = tar_fea + den_fea + # tot_fea = tad_fea + spa_fea + if dataset == "kaggle": + days = 7 + out_file = "kaggleAdDisplayChallenge_processed" + elif dataset == "terabyte": + days = 24 + out_file = "terabyte_processed" + else: + raise(ValueError("Data set option is not supported")) + self.max_ind_range = max_ind_range + self.memory_map = memory_map + + # split the datafile into path and filename + lstr = raw_path.split("/") + self.d_path = "/".join(lstr[0:-1]) + "/" + self.d_file = lstr[-1].split(".")[0] if dataset == "kaggle" else lstr[-1] + self.npzfile = self.d_path + ( + (self.d_file + "_day") if dataset == "kaggle" else self.d_file + ) + self.trafile = self.d_path + ( + (self.d_file + "_fea") if dataset == "kaggle" else "fea" + ) + + # check if pre-processed data is available + data_ready = True + if memory_map: + for i in range(days): + reo_data = self.npzfile + "_{0}_reordered.npz".format(i) + if not path.exists(str(reo_data)): + data_ready = False + else: + if not path.exists(str(pro_data)): + data_ready = False + + # pre-process data if needed + # WARNNING: when memory mapping is used we get a collection of files + if data_ready: + logging.debug("Reading pre-processed data=%s" % (str(pro_data))) + file = str(pro_data) + else: + logging.debug("Reading raw data=%s" % (str(raw_path))) + file = data_utils.getCriteoAdData( + raw_path, + out_file, + max_ind_range, + sub_sample_rate, + days, + split, + randomize, + dataset == "kaggle", + memory_map, + dataset_multiprocessing, + ) + + # get a number of samples per day + total_file = self.d_path + self.d_file + "_day_count.npz" + with np.load(total_file) as data: + total_per_file = data["total_per_file"] + # compute offsets per file + self.offset_per_file = np.array([0] + [x for x in total_per_file]) + for i in range(days): + self.offset_per_file[i + 1] += self.offset_per_file[i] + # logging.debug(self.offset_per_file) + + # setup data + if memory_map: + # setup the training/testing split + self.split = split + if split == 'none' or split == 'train': + self.day = 0 + self.max_day_range = days if split == 'none' else days - 1 + elif split == 'test' or split == 'val': + self.day = days - 1 + num_samples = self.offset_per_file[days] - self.offset_per_file[days - 1] + self.test_size = int(np.ceil(num_samples / 2.)) + self.val_size = num_samples - self.test_size + else: + sys.exit("ERROR: dataset split is neither none, nor train or test.") + + ''' + # text + logging.debug("text") + for i in range(days): + fi = self.npzfile + "_{0}".format(i) + with open(fi) as data: + ttt = 0; nnn = 0 + for _j, line in enumerate(data): + ttt +=1 + if np.int32(line[0]) > 0: + nnn +=1 + logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" + + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") + # processed + logging.debug("processed") + for i in range(days): + fi = self.npzfile + "_{0}_processed.npz".format(i) + with np.load(fi) as data: + yyy = data["y"] + ttt = len(yyy) + nnn = np.count_nonzero(yyy) + logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" + + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") + # reordered + logging.debug("reordered") + for i in range(days): + fi = self.npzfile + "_{0}_reordered.npz".format(i) + with np.load(fi) as data: + yyy = data["y"] + ttt = len(yyy) + nnn = np.count_nonzero(yyy) + logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" + + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") + ''' + + # load unique counts + with np.load(self.d_path + self.d_file + "_fea_count.npz") as data: + self.counts = data["counts"] + self.m_den = den_fea # X_int.shape[1] + self.n_emb = len(self.counts) + logging.debug("Sparse features= %d, Dense features= %d" % (self.n_emb, self.m_den)) + + # Load the test data + # Only a single day is used for testing + if self.split == 'test' or self.split == 'val': + # only a single day is used for testing + fi = self.npzfile + "_{0}_reordered.npz".format( + self.day + ) + with np.load(fi) as data: + self.X_int = data["X_int"] # continuous feature + self.X_cat = data["X_cat"] # categorical feature + self.y = data["y"] # target + + else: + # load and preprocess data + with np.load(file) as data: + X_int = data["X_int"] # continuous feature + X_cat = data["X_cat"] # categorical feature + y = data["y"] # target + self.counts = data["counts"] + self.m_den = X_int.shape[1] # den_fea + self.n_emb = len(self.counts) + logging.debug("Sparse fea = %d, Dense fea = %d" % (self.n_emb, self.m_den)) + + # create reordering + indices = np.arange(len(y)) + + self.train_indices = None + self.test_indices = None + self.val_indices = None + if store_all_indices: + indices = np.array_split(indices, self.offset_per_file[1:-1]) + + # randomize train data (per day) + if randomize == "day": # or randomize == "total": + for i in range(len(indices) - 1): + indices[i] = np.random.permutation(indices[i]) + logging.debug("Randomized indices per day ...") + + self.train_indices = np.concatenate(indices[:-1]) + self.test_indices = indices[-1] + self.test_indices, self.val_indices = np.array_split(self.test_indices, 2) + + logging.debug("Defined %s indices..." % (split)) + self.X_int = X_int + self.X_cat = X_cat + self.y = y + # randomize train data (across days) + if randomize == "total": + train_indices = np.random.permutation(self.train_indices) + logging.debug("Randomized indices across days ...") + + elif split == "none": + # randomize all data + if randomize == "total": + indices = np.random.permutation(indices) + logging.debug("Randomized indices...") + + X_int[indices] = X_int + X_cat[indices] = X_cat + y[indices] = y + + else: + indices = np.array_split(indices, self.offset_per_file[1:-1]) + + # randomize train data (per day) + if randomize == "day": # or randomize == "total": + for i in range(len(indices) - 1): + indices[i] = np.random.permutation(indices[i]) + logging.debug("Randomized indices per day ...") + + train_indices = np.concatenate(indices[:-1]) + test_indices = indices[-1] + test_indices, val_indices = np.array_split(test_indices, 2) + + logging.debug("Defined %s indices..." % (split)) + + # randomize train data (across days) + if randomize == "total": + train_indices = np.random.permutation(train_indices) + logging.debug("Randomized indices across days ...") + + # create training, validation, and test sets + if split == 'train': + self.X_int = [X_int[i] for i in train_indices] + self.X_cat = [X_cat[i] for i in train_indices] + self.y = [y[i] for i in train_indices] + elif split == 'val': + self.X_int = [X_int[i] for i in val_indices] + self.X_cat = [X_cat[i] for i in val_indices] + self.y = [y[i] for i in val_indices] + elif split == 'test': + self.X_int = [X_int[i] for i in test_indices] + self.X_cat = [X_cat[i] for i in test_indices] + self.y = [y[i] for i in test_indices] + + logging.debug("Split data according to indices...") + + def __getitem__(self, index): + + if isinstance(index, slice): + return [ + self[idx] for idx in range( + index.start or 0, index.stop or len(self), index.step or 1 + ) + ] + + if self.memory_map: + if self.split == 'none' or self.split == 'train': + # check if need to swicth to next day and load data + if index == self.offset_per_file[self.day]: + # logging.debug("day_boundary switch", index) + self.day_boundary = self.offset_per_file[self.day] + fi = self.npzfile + "_{0}_reordered.npz".format( + self.day + ) + # logging.debug('Loading file: ', fi) + with np.load(fi) as data: + self.X_int = data["X_int"] # continuous feature + self.X_cat = data["X_cat"] # categorical feature + self.y = data["y"] # target + self.day = (self.day + 1) % self.max_day_range + + i = index - self.day_boundary + elif self.split == 'test' or self.split == 'val': + # only a single day is used for testing + i = index + (0 if self.split == 'test' else self.test_size) + else: + sys.exit("ERROR: dataset split is neither none, nor train or test.") + else: + i = index + + if self.max_ind_range > 0: + return self.X_int[i], self.X_cat[i] % self.max_ind_range, self.y[i] + else: + return self.X_int[i], self.X_cat[i], self.y[i] + + def _default_preprocess(self, X_int, X_cat, y): + X_int = torch.log(torch.tensor(X_int, dtype=torch.float) + 1) + if self.max_ind_range > 0: + X_cat = torch.tensor(X_cat % self.max_ind_range, dtype=torch.long) + else: + X_cat = torch.tensor(X_cat, dtype=torch.long) + y = torch.tensor(y.astype(np.float32)) + + return X_int, X_cat, y + + def __len__(self): + if self.memory_map: + if self.split == 'none': + return self.offset_per_file[-1] + elif self.split == 'train': + return self.offset_per_file[-2] + elif self.split == 'test': + return self.test_size + elif self.split == 'val': + return self.val_size + else: + sys.exit("ERROR: dataset split is neither none, nor train nor test.") + else: + return len(self.y) + + +def collate_wrapper_criteo_offset(list_of_tuples): + # where each tuple is (X_int, X_cat, y) + # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) + # transposed_data = list(zip(*list_of_tuples)) + + transposed_data = list(np.array(i) for i in zip(*list_of_tuples)) + X_int = torch.log(torch.tensor(transposed_data[0], dtype=torch.float) + 1) + X_cat = torch.tensor(transposed_data[1], dtype=torch.long) + T = torch.tensor(transposed_data[2], dtype=torch.float32).view(-1, 1) + + batchSize = X_cat.shape[0] + featureCnt = X_cat.shape[1] + + lS_i = [X_cat[:, i] for i in range(featureCnt)] + lS_o = [torch.tensor(range(batchSize)) for _ in range(featureCnt)] + gc.collect() + return X_int, torch.stack(lS_i), torch.stack(lS_o), T + + +# Conversion from offset to length +def offset_to_length_converter(lS_o, lS_i): + def diff(tensor): + return tensor[1:] - tensor[:-1] + + return torch.stack( + [ + diff(torch.cat((S_o, torch.tensor(lS_i[ind].shape))).int()) + for ind, S_o in enumerate(lS_o) + ] + ) + + +def collate_wrapper_criteo_length(list_of_tuples): + # where each tuple is (X_int, X_cat, y) + # transposed_data = list(zip(*list_of_tuples)) + + transposed_data = list(np.array(i) for i in zip(*list_of_tuples)) + # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) + X_int = torch.log(torch.tensor(transposed_data[0], dtype=torch.float) + 1) + X_cat = torch.tensor(transposed_data[1], dtype=torch.long) + T = torch.tensor(transposed_data[2], dtype=torch.float32).view(-1, 1) + + batchSize = X_cat.shape[0] + featureCnt = X_cat.shape[1] + + lS_i = torch.stack([X_cat[:, i] for i in range(featureCnt)]) + lS_o = torch.stack( + [torch.tensor(range(batchSize)) for _ in range(featureCnt)] + ) + + lS_l = offset_to_length_converter(lS_o, lS_i) + + return X_int, lS_l, lS_i, T diff --git a/examples/dlrm/requirements.txt b/examples/dlrm/requirements.txt new file mode 100644 index 0000000..7308aef --- /dev/null +++ b/examples/dlrm/requirements.txt @@ -0,0 +1,6 @@ +bitorch +fvbitcore +pytorch_lightning +sklearn +torchmetrics +wandb diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py new file mode 100644 index 0000000..11b7a51 --- /dev/null +++ b/examples/dlrm/train_dlrm.py @@ -0,0 +1,230 @@ +import os + +if os.environ.get("REMOTE_PYCHARM_DEBUG_SESSION", False): + import pydevd_pycharm + + pydevd_pycharm.settrace( + "localhost", + port=int(os.environ.get("REMOTE_PYCHARM_DEBUG_PORT", "12345")), + stdoutToServer=True, + stderrToServer=True, + ) + +import argparse +import logging +from pathlib import Path +from typing import List, Any + +import fvbitcore.nn as fv_nn +import wandb +from pytorch_lightning import Trainer +from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor +from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase, WandbLogger +from torch.utils.data import DataLoader + +from bitorch import apply_args_to_configuration +from bitorch.datasets import dataset_from_name +from bitorch.models import DLRM +from bitorch.quantizations import Quantization + +from utils.arg_parser import create_argparser +from utils.lightning_model import ModelWrapper +from utils.utils import configure_logging +from utils.log import CommandLineLogger + +from criteo import Criteo, SplitCriteoDataset +from facebook_dataloading.dataloading_fb import collate_wrapper_criteo_offset +from bitorch.datasets import datasets_by_name + +logger = logging.getLogger() + + +def make_dlrm_dataloaders(dataset_dir, download, ignore_size, batch_size, batch_size_test, num_workers): + # train_dataset, test_dataset = Criteo.get_train_and_test( # type: ignore + # root_directory=dataset_dir, download=download, augmentation=None + # ) + logging.info("loading Criteo dataset...") + dataset = Criteo(True, root_directory=dataset_dir, download=download, augmentation=None).dataset + + # train_indices = np.arange(len(train_dataset)) + # np.random.shuffle(train_indices) + # train_subset_random_sampler = SubsetRandomSampler(train_indices[:int((1 - ignore_size) * len(train_dataset))]) + + # val_indices = np.arange(len(test_dataset)) + # np.random.shuffle(val_indices) + # val_subset_random_sampler = SubsetRandomSampler(val_indices[:int((1 - ignore_size) * len(test_dataset))]) + + train_dataset = SplitCriteoDataset(dataset, "train", ignore_size=ignore_size) + test_dataset = SplitCriteoDataset(dataset, "test", ignore_size=ignore_size) + logging.info(f"loaded {len(train_dataset)} train and {len(test_dataset)} test samples!") + + train_loader = DataLoader( + train_dataset, + batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + collate_fn=collate_wrapper_criteo_offset, + pin_memory=False, + drop_last=False, + ) + + test_loader = DataLoader( + test_dataset, + batch_size=batch_size_test, + shuffle=False, + num_workers=num_workers, + collate_fn=collate_wrapper_criteo_offset, + pin_memory=False, + drop_last=False, + ) + + return train_loader, test_loader, train_dataset.dataset.m_den, train_dataset.dataset.counts + + +def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: + """trains a model on the configured image dataset. + + Args: + args (argparse.Namespace): cli arguments + model_args (argparse.Namespace): model specific cli arguments + """ + configure_logging(logger, args.log_file, args.log_level, args.log_stdout) + + apply_args_to_configuration(args) + + output_dir = Path(args.result_directory) + output_dir.mkdir(exist_ok=True) + + loggers: List[LightningLoggerBase] = [] + if args.tensorboard_log: + loggers.append(TensorBoardLogger(str(output_dir), name="tensorboard")) + if args.csv_log: + loggers.append(CSVLogger(str(output_dir), name="csv")) + if args.wandb_log: + loggers.append( + WandbLogger( + project=args.wandb_project, + log_model=True, + name=args.wandb_experiment, + save_dir=str(output_dir), + ) # type: ignore + ) + callbacks: List[Any] = [] + if args.checkpoint_dir is not None: + callbacks.append( + ModelCheckpoint( + args.checkpoint_dir, + save_last=True, + save_top_k=args.checkpoint_keep_count, + monitor="metrics/roc-auc", + ) + ) + + # providing our own progress bar disables the default progress bar (not needed to disable later on) + cmd_logger = CommandLineLogger(args.log_interval) + callbacks.append(cmd_logger) + configure_logging(cmd_logger.logger, args.log_file, args.log_level, args.log_stdout) + + if len(loggers) > 0: + lr_monitor = LearningRateMonitor(logging_interval="step") + callbacks.append(lr_monitor) + + datasets_by_name["criteo"] = Criteo + dataset = dataset_from_name(args.dataset) + if args.dataset == "criteo": + train_loader, test_loader, dense_feature_size, embedding_layer_sizes = make_dlrm_dataloaders( + args.dataset_dir, + args.download, + args.ignore_dataset_size, + args.batch_size, + args.batch_size_test, + args.num_workers, + ) + else: + logging.error(f"dataset {args.dataset} is not yet supported for dlrm") + return + + model_kwargs = vars(model_args) + logger.debug(f"got model args as dict: {model_kwargs}") + + model = DLRM(**model_kwargs, embedding_layer_sizes=embedding_layer_sizes, dataset=dataset, dense_feature_size=dense_feature_size) # type: ignore + model.initialize() + if args.checkpoint_load is not None and args.pretrained: + logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") + model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) + else: + model_wrapped = ModelWrapper(model, 1, args) + + trainer = Trainer( + strategy=args.strategy, + accelerator="cpu" if args.cpu else args.accelerator, + gpus=0 if args.cpu else args.gpus, + max_epochs=args.max_epochs, + max_steps=args.max_steps, + logger=loggers if len(loggers) > 0 else None, # type: ignore + callbacks=callbacks, # type: ignore + log_every_n_steps=args.log_interval, + ) + # augmentation_level = Augmentation.from_string(args.augmentation) + logger.info(f"model: {args.model}") + logger.info(f"optimizer: {args.optimizer}") + logger.info(f"lr: {args.lr}") + logger.info(f"max_epochs: {args.max_epochs}") + # if args.fake_data: + # logger.info(f"dummy dataset: {dataset.name} (not using real data!)") + # train_dataset, test_dataset = dataset.get_dummy_train_and_test_datasets() # type: ignore + # else: + # logger.info(f"dataset: {dataset.name}") + # train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore + # root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level + # ) + # train_loader = DataLoader( + # train_dataset, + # batch_size=args.batch_size, + # num_workers=args.num_workers, + # shuffle=True, + # pin_memory=True, + # persistent_workers=True, + # ) # type: ignore + # test_loader = DataLoader( + # test_dataset, + # batch_size=args.batch_size, + # num_workers=args.num_workers, + # shuffle=False, + # pin_memory=True, + # persistent_workers=True, + # ) # type: ignore + + data_point = iter(train_loader).next() + data_point = (data_point[0], (data_point[1], data_point[2])) + # data_point = torch.zeros(iter(train_loader).next().shape) + computational_intensity = fv_nn.FlopCountAnalysis(model, inputs=data_point, quantization_base_class=Quantization) + + stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) + logger.info("\n" + table) + total_size = stats["#compressed size in bits"][""] + logger.info("Total size in MB: " + str(total_size / 1e6 / 8.0)) + total_flops = stats["#speed up flops (app.)"][""] + logger.info("Approximated mflops: " + str(total_flops / 1e6)) + if args.wandb_log: + wandb.config.update( + { + "mflops": total_flops / 1e6, + "size in MB": total_size / 1e6 / 8.0, + } + ) + + trainer.fit( + model_wrapped, + train_dataloaders=train_loader, + val_dataloaders=test_loader, + ckpt_path=args.checkpoint_load if not args.pretrained else None, + ) + + +if __name__ == "__main__": + parser, model_parser = create_argparser() + args_, unparsed_model_args = parser.parse_known_args() + model_args_ = model_parser.parse_args(unparsed_model_args) + + main(args_, model_args_) diff --git a/examples/dlrm/utils/__init__.py b/examples/dlrm/utils/__init__.py new file mode 100644 index 0000000..f85768d --- /dev/null +++ b/examples/dlrm/utils/__init__.py @@ -0,0 +1 @@ +"""Utilities for image classification example.""" diff --git a/examples/dlrm/utils/arg_parser.py b/examples/dlrm/utils/arg_parser.py new file mode 100644 index 0000000..f0c562d --- /dev/null +++ b/examples/dlrm/utils/arg_parser.py @@ -0,0 +1,305 @@ +from argparse import ArgumentParser +import sys +from typing import Tuple + +from bitorch.models import model_from_name, model_names +from bitorch.datasets import dataset_names +from bitorch import add_config_args +from pytorch_lightning import Trainer + + +def add_logging_args(parser: ArgumentParser) -> None: + """adds cli parameters for logging configuration + + Args: + parser (ArgumentParser): the main argument parser + """ + log = parser.add_argument_group("Logging", "parameters for logging") + log.add_argument( + "--log-level", + type=str, + default="info", + choices=["debug", "info", "warning", "error", "critical"], + help="log level for logging message output", + ) + log.add_argument( + "--log-interval", + type=int, + default=100, + metavar="N", + help="how many batches to wait before logging training status", + ) + log.add_argument( + "--log-file", + type=str, + default=None, + help="output file path for logging. default to stdout", + ) + log.add_argument( + "--log-stdout", + action="store_true", + help="toggles force logging to stdout. if a log file is specified, logging will be" + "printed to both the log file and stdout", + ) + log.add_argument( + "--result-directory", + type=str, + default="./logs", + help="path to logs directory, e.g. tensorboard logs, csv files", + ) + log.add_argument( + "--disable-tensorboard-log", + action="store_false", + dest="tensorboard_log", + help="disables tensorboard logging", + ) + log.add_argument( + "--disable-csv-log", + action="store_false", + dest="csv_log", + help="disables csv logging", + ) + log.add_argument( + "--wandb", + action="store_true", + dest="wandb_log", + help="enables wandb logging (WANDB_API_KEY environment variable must be set)", + ) + log.add_argument( + "--wandb-project", + type=str, + default="bitorch", + help="name of wand project to be used by wandb logger", + ) + log.add_argument( + "--wandb-experiment", + type=str, + default=None, + help="name of wand experiment to be used by wandb logger", + ) + + +def add_checkpoint_args(parser: ArgumentParser) -> None: + """adds cli parameters for checkpoint logging + + Args: + parser (ArgumentParser): the main argument parser + """ + checkpoint = parser.add_argument_group("checkpoints", "parameters for checkpoint storing / loading") + checkpoint.add_argument( + "--checkpoint-dir", + type=str, + default=None, + help="set a custom path to store checkpoints in.", + ) + checkpoint.add_argument( + "--checkpoint-keep-count", + type=int, + default=1, + help="number of checkpoints to keep.", + ) + checkpoint.add_argument( + "--checkpoint-load", + type=str, + default=None, + help="path to checkpoint file to load state from. if omitted, a new model will be trained.", + ) + checkpoint.add_argument( + "--pretrained", + action="store_true", + help="uses the given checkpoint as a pretrained model (only for initialization)", + ) + + +def add_optimizer_args(parser: ArgumentParser) -> None: + """adds cli parameters for optimizer configuration + + Args: + parser (ArgumentParser): the main argument parser + """ + optimizer = parser.add_argument_group("Optimizer", "parameters for optimizer") + optimizer.add_argument( + "--lr-scheduler", + type=str, + choices=["cosine", "step", "exponential"], + help="name of the lr scheduler to use. default to none", + ) + optimizer.add_argument( + "--lr", + type=float, + default=0.01, + help="initial learning rate (default: 0.01)", + ) + optimizer.add_argument( + "--lr-factor", + default=0.1, + type=float, + help="learning rate decay ratio. this is used only by the step and exponential lr scheduler", + ) + optimizer.add_argument( + "--lr-steps", + nargs="*", + default=[30, 60, 90], + help="list of learning rate decay epochs as list. this is used only by the step scheduler", + ) + optimizer.add_argument( + "--momentum", + type=float, + default=0.9, + help="momentum value for optimizer, default is 0.9. only used for sgd optimizer", + ) + optimizer.add_argument( + "--optimizer", + type=str, + default="adam", + choices=["adam", "sgd", "radam"], + help="the optimizer to use. default is adam.", + ) + + +def add_dataset_args(parser: ArgumentParser) -> None: + """adds cli parameters for dataset configuration + + Args: + parser (ArgumentParser): the main argument parser + """ + data = parser.add_argument_group("dataset", "parameters for the dataset used for training") + data.add_argument( + "--dataset", + type=str, + default="cifar10", + choices=dataset_names() + ["criteo"], + help="name of the dataset to be used for training", + ) + data.add_argument( + "--dataset-dir", + type=str, + default=None, + help="path to where the train dataset is saved / shall be downloaded to", + ) + data.add_argument( + "--download", + action="store_true", + help="toggles wether the dataset shall be downloaded if not present. " + "only has effect with the cifar10 and mnist dataset so far.", + ) + data.add_argument( + "--batch-size", + type=int, + default=128, + help="batch size for training (default: 128)", + ) + data.add_argument( + "--batch-size-test", + type=int, + default=128, + help="batch size for testing (might be higher than training) (default: 128)", + ) + data.add_argument( + "--ignore-dataset-size", + type=float, + default=0.9, + help="portion of dataset that should be ignored for training (usefull for fast development) (default: 128)", + ) + data.add_argument( + "--num-workers", + type=int, + default=4, + help="number of workers to be used for dataloading (default: 4)", + ) + data.add_argument( + "--augmentation", + type=str, + choices=["none", "low", "medium", "high"], + default="none", + help="level of augmentation to be used in data preparation (default 'none')", + ) + data.add_argument( + "--fake-data", + action="store_true", + help="train with fake data", + ) + + +def create_model_argparser(model_class: object) -> ArgumentParser: + """adds model specific cli arguments from model_class object + + Args: + model_class (object): the class-object of selected model + + Returns: + ArgumentParser: cli argument parser + """ + model_parser = ArgumentParser(add_help=False) + model_class.add_argparse_arguments(model_parser) + return model_parser + + +def help_in_args() -> bool: + """determines if script was called with a --help or -h flag + + Returns: + bool: True if help flag was set, False otherwise + """ + passed_args = sys.argv[1:] + if "--help" in passed_args or "-h" in passed_args: + return True + return False + + +def add_all_model_args(parser: ArgumentParser) -> None: + """iterates through all existent models and adds their specific cli args to parser + + Args: + parser (ArgumentParser): the main cli argument parser + """ + for model_name in model_names(): + model_group = parser.add_argument_group(model_name, f"parameters for {model_name} model") + model_from_name(model_name).add_argparse_arguments(model_group) # type: ignore + + +def add_regular_args(parser: ArgumentParser) -> None: + """adds all regular arguments, including dynamically created config args to parser. + + Args: + parser (ArgumentParser): parser to add the regular arguments to + """ + Trainer.add_argparse_args(parser) + add_logging_args(parser) + add_dataset_args(parser) + add_optimizer_args(parser) + add_checkpoint_args(parser) + + add_config_args(parser) + + parser.add_argument( + "--model", + type=str.lower, + choices=model_names(), + required=True, + help="name of the model to be trained", + ) + parser.add_argument( + "--cpu", + action="store_true", + help="explicitly use the cpu. overwrites gpu settings", + ) + + +def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: + """creates a main argument parser for general options and a model parser for model specific options + + Returns: + Tuple[ArgumentParser, ArgumentParser]: the main and model argument parser + """ + parser = ArgumentParser(description="Bitorch Image Classification") + + add_regular_args(parser) + + if help_in_args(): + add_all_model_args(parser) + args, _ = parser.parse_known_args() + + model_class = model_from_name(args.model) + model_parser = create_model_argparser(model_class) + return parser, model_parser diff --git a/examples/dlrm/utils/lightning_model.py b/examples/dlrm/utils/lightning_model.py new file mode 100644 index 0000000..968f9c6 --- /dev/null +++ b/examples/dlrm/utils/lightning_model.py @@ -0,0 +1,120 @@ +import numpy as np +from sklearn import metrics +import logging +from argparse import Namespace +from typing import Union + +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from torch.nn import Module, BCELoss +from torchmetrics import Accuracy, F1Score, Precision, Recall + +from .unused_args import clean_hyperparameters +from .utils import create_optimizer, create_scheduler + + +class ModelWrapper(LightningModule): + def __init__( + self, + model: Module, + num_classes: int, + args: Namespace, + add_f1_prec_recall: bool = False, + ) -> None: + super().__init__() + self.save_hyperparameters(clean_hyperparameters(args)) + self.loss_function = BCELoss() + self.model = model + self.train_accuracy_top1 = Accuracy(num_classes=num_classes) + self.train_accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) + self.accuracy_top1 = Accuracy(num_classes=num_classes) + self.accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) + self.add_f1_prec_recall = add_f1_prec_recall + if add_f1_prec_recall: + self.f1 = F1Score(num_classes=num_classes) + self.prec = Precision(num_classes=num_classes) + self.recall = Recall(num_classes=num_classes) + + def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: ignore + logging.info(f"Using {self.hparams.optimizer} optimizer and {self.hparams.lr_scheduler} lr scheduler...") + optimizer = create_optimizer(self.hparams.optimizer, self.model, self.hparams.lr, self.hparams.momentum) + if self.hparams.lr_scheduler is not None: + scheduler = create_scheduler( + self.hparams.lr_scheduler, + optimizer, + self.hparams.lr_factor, + self.hparams.lr_steps, + self.hparams.max_epochs, + ) + return {"optimizer": optimizer, "lr_scheduler": scheduler} + else: + return optimizer + + def training_step(self, batch, batch_idx): + dense_values, categorical_values_i, categorical_values_o, y = batch + if isinstance(categorical_values_i, list): + for el in categorical_values_i: + el.to(self.device) + else: + categorical_values_i = categorical_values_i.to(self.device) + if isinstance(categorical_values_o, list): + for el in categorical_values_o: + el.to(self.device) + else: + categorical_values_o = categorical_values_o.to(self.device) + dense_values.to(self.device) + y_hat = self.model(dense_values, (categorical_values_i, categorical_values_o)) + + loss = self.loss_function(torch.squeeze(y_hat), torch.squeeze(y)) + self.log_dict({"loss/train": loss}) + return loss + + def validation_step_end(self, *args, **kwargs): + """calculate all them metrics and log via wandb/tensorboard""" + + y = torch.cat(list(map(lambda x: x["y"], self.validation_results))) + y_hat = torch.cat(list(map(lambda x: x["y_hat"], self.validation_results))) + loss = self.loss_function(y, y_hat) + rmse = torch.sqrt(F.mse_loss(y_hat, y)).item() + y_array = np.array(y.cpu()) + y_hat_array = np.array(y_hat.cpu()) >= 0.5 + balanced_accuracy = metrics.balanced_accuracy_score(y_array, y_hat_array) + accuracy = metrics.accuracy_score(y_array, y_hat_array) + f1 = metrics.f1_score(y_array, y_hat_array) + roc_auc = metrics.roc_auc_score(y_array, y_hat.cpu()) + precision = metrics.precision_score(y_array, y_hat_array) + recall = metrics.recall_score(y_array, y_hat_array) + self.log_dict({ + "val_los": loss, + "val_rmse": rmse, + "roc_auc": roc_auc, + "precision": precision, + "recall": recall, + "balanced accuracy": balanced_accuracy, + "accuracy": accuracy, + "f1 score": f1, + }, prog_bar=True) + return super().validation_step_end(*args, **kwargs) + + def on_validation_start(self) -> None: + self.validation_results = [] + return super().on_validation_start() + + def validation_step(self, batch, batch_idx): + dense_values, categorical_values_i, categorical_values_o, y = batch + dense_values = dense_values.to(self.device) + if isinstance(categorical_values_i, list): + for el in categorical_values_i: + el.to(self.device) + else: + categorical_values_i = categorical_values_i.to(self.device) + if isinstance(categorical_values_o, list): + for el in categorical_values_o: + el.to(self.device) + else: + categorical_values_o = categorical_values_o.to(self.device) + y_hat = torch.squeeze(self.model(dense_values, (categorical_values_i, categorical_values_o))) + y = torch.squeeze(y) + y_hat = torch.squeeze(y_hat) + self.validation_results.append({"y": y, "y_hat": y_hat}) diff --git a/examples/dlrm/utils/log.py b/examples/dlrm/utils/log.py new file mode 100644 index 0000000..ea49921 --- /dev/null +++ b/examples/dlrm/utils/log.py @@ -0,0 +1,177 @@ +import logging +import time +from typing import Optional, Any, Dict, List, Union + +import math +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ProgressBarBase +from pytorch_lightning.utilities.types import STEP_OUTPUT + + +TIME_INTERVALS = ( + ("w", 60 * 60 * 24 * 7), + ("d", 60 * 60 * 24), + ("h", 60 * 60), + ("m", 60), + ("s", 1), +) + + +def _display_time(seconds: float, granularity: int = 2) -> str: + result: List[str] = [] + + seconds = int(round(seconds)) + + for name, count in TIME_INTERVALS: + value = seconds // count + if value == 0 and len(result) == 0: + continue + seconds -= value * count + result.append(f"{value:02d}{name}") + return ":".join(result[:granularity]) + + +class CommandLineLogger(ProgressBarBase): + """ + This module provides a replacement for the default tqdm-based progress bar, that is more suitable for logging + progress in a non-interactive way, e.g. to a file. + """ + + def __init__(self, refresh_rate: int) -> None: + super().__init__() + self._is_enabled = True + self._epoch_start_time: float = 0.0 + self._validation_start_time: float = 0.0 + self._train_start_time: float = 0.0 + self._last_epoch_times: List[float] = [] + self._validation_times: List[float] = [] + + self.logger = logging.getLogger("CommandLineLogger") + + self.refresh_rate = refresh_rate + self.log_batch = self.log_info + self.log_debug("Command line logger initialized.") + + def log_debug(self, message: str) -> None: + if self._is_enabled: + # self.logger.debug(message) + print(message) + + def log_info(self, message: str) -> None: + if self._is_enabled: + # self.logger.info(message) + print(message) + + def setup(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", stage: Optional[str] = None) -> None: + self.log_debug(f"Command line logger setup. ( is root trainer: {trainer.is_global_zero} )") + super().setup(trainer, pl_module, stage) + + def disable(self) -> None: + self.log_debug("Logging disabled...") + self._is_enabled = False + + def enable(self) -> None: + self._is_enabled = True + self.log_debug("Logging enabled...") + + def _should_update(self, current: int, total: Union[int, float]) -> bool: + return self._is_enabled and (current % self.refresh_rate == 0 or current == int(total)) + + def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self.log_info("Starting training...") + + def on_train_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self.log_info("Ending training.") + + def on_train_batch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + outputs: STEP_OUTPUT, + batch: Any, + batch_idx: int, + unused: int = 0, + ) -> None: + if not self._should_update(self.train_batch_idx, self.total_train_batches): + return + + percent = (self.train_batch_idx / self.total_train_batches) * 100 + + time_in_this_epoch = time.time() - self._epoch_start_time + epoch_total_est = int(round((time_in_this_epoch * self.total_train_batches) / self.train_batch_idx)) + eta_epoch = _display_time(epoch_total_est - time_in_this_epoch) + full_epochs_left = trainer.max_epochs - trainer.current_epoch + if full_epochs_left < 0: + full_epochs_left = 0 + if self._average_epoch_time() > 0: + epoch_total_est = self._average_epoch_time() + self._average_validation_time() + eta_train = _display_time(epoch_total_est - time_in_this_epoch + full_epochs_left * epoch_total_est) + + epoch_info = f"Epoch {trainer.current_epoch:3d}" + batch_info = f"{self.train_batch_idx:4d}/{self.total_train_batches:4d} ({percent:5.1f}%)" + metrics = self._format_metric_string(self.get_metrics(trainer, pl_module)) + eta_info = f"ETA: {eta_epoch} & {eta_train}" + self.log_batch(f"{epoch_info} - {batch_info} - {metrics} - {eta_info}") + + @staticmethod + def _replace_metric_key(metric_key: str) -> str: + remove_strings = [ + "metrics/", + ] + for s in remove_strings: + metric_key = metric_key.replace(s, "") + return metric_key.replace("accuracy", "acc") + + @staticmethod + def _format_metric_string(metrics_dict: Dict[str, Union[int, str]], train: bool = True) -> str: + metric_list = [] + + for key, value in metrics_dict.items(): + if key == "v_num" or "loss" in key: + continue + key = CommandLineLogger._replace_metric_key(key) + try: + f_value = float(value) + if math.isnan(f_value): + continue + if key: + metric_list.append(f"{key}={f_value:2.2f}") + except ValueError: + if key: + metric_list.append(f"{key}={value}") + + return ", ".join(metric_list) + + @staticmethod + def _average_time(time_list: List[float]) -> int: + return int(round(sum(time_list) / len(time_list))) + + def _average_epoch_time(self) -> int: + if len(self._last_epoch_times) == 0: + return 0 + return self._average_time(self._last_epoch_times) + + def _average_validation_time(self) -> int: + if len(self._validation_times) == 0: + return 0 + return self._average_time(self._validation_times) + + def on_train_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self._epoch_start_time = time.time() + if self._train_start_time is None: + self._train_start_time = self._epoch_start_time + + def on_train_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self._last_epoch_times.append(time.time() - self._epoch_start_time) + self._last_epoch_times = self._last_epoch_times[-3:] + + def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self._validation_start_time = time.time() + self.log_info("Validating model...") + + def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + self._validation_times.append(time.time() - self._validation_start_time) + self._validation_times = self._validation_times[-3:] + self.log_info( + f"Validation complete. ({self._format_metric_string(self.get_metrics(trainer, pl_module), train=False)})" + ) diff --git a/examples/dlrm/utils/unused_args.py b/examples/dlrm/utils/unused_args.py new file mode 100644 index 0000000..7adeb79 --- /dev/null +++ b/examples/dlrm/utils/unused_args.py @@ -0,0 +1,73 @@ +"""Args from PyTorch Lightning's Trainer that are currently unused and a function to deal with them.""" +from argparse import Namespace +from typing import List + + +def clean_hyperparameters(args: Namespace) -> Namespace: + """Remove args which are not passed to the constructor in our training script.""" + clean_args = Namespace() + for key in args.__dict__.keys(): + if key in UNUSED_PL_ARGS: + continue + setattr(clean_args, key, getattr(args, key)) + return clean_args + + +# this list is copied from the constructor of PyTorch's Trainer, but all arguments used in our script were removed +UNUSED_PL_ARGS: List[str] = [ + "logger", + "checkpoint_callback", + "enable_checkpointing", + "callbacks", + "default_root_dir", + "gradient_clip_val", + "gradient_clip_algorithm", + "process_position", + "num_nodes", + "num_processes", + "devices", + "auto_select_gpus", + "tpu_cores", + "ipus", + "log_gpu_memory", + "progress_bar_refresh_rate", + "enable_progress_bar", + "overfit_batches", + "track_grad_norm", + "check_val_every_n_epoch", + "fast_dev_run", + "accumulate_grad_batches", + "min_epochs", + "min_steps", + "max_time", + "limit_train_batches", + "limit_val_batches", + "limit_test_batches", + "limit_predict_batches", + "val_check_interval", + "flush_logs_every_n_steps", + "log_every_n_steps", + "sync_batchnorm", + "precision", + "enable_model_summary", + "weights_summary", + "weights_save_path", + "num_sanity_val_steps", + "resume_from_checkpoint", + "profiler", + "benchmark", + "deterministic", + "reload_dataloaders_every_n_epochs", + "auto_lr_find", + "replace_sampler_ddp", + "detect_anomaly", + "auto_scale_batch_size", + "prepare_data_per_node", + "plugins", + "amp_backend", + "amp_level", + "move_metrics_to_cpu", + "multiple_trainloader_mode", + "stochastic_weight_avg", + "terminate_on_nan", +] diff --git a/examples/dlrm/utils/utils.py b/examples/dlrm/utils/utils.py new file mode 100644 index 0000000..2a55bbb --- /dev/null +++ b/examples/dlrm/utils/utils.py @@ -0,0 +1,105 @@ +import logging +from pathlib import Path + +from torch.optim import Adam, SGD, RAdam +from torch.optim.lr_scheduler import MultiStepLR, ExponentialLR, CosineAnnealingLR, _LRScheduler +from typing import Union, Optional, Any +from torch.nn import Module +from torch.optim.optimizer import Optimizer + + +def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, output_stdout: bool) -> None: + """configures logging module. + + Args: + logger: the logger to be configured + log_file (str): path to log file. if omitted, logging will be forced to stdout. + log_level (str): string name of log level (e.g. 'debug') + output_stdout (bool): toggles stdout output. will be activated automatically if no log file was given. + otherwise if activated, logging will be outputed both to stdout and log file. + """ + log_level_name = log_level.upper() + log_level = getattr(logging, log_level_name) + logger.setLevel(log_level) + + logging_format = logging.Formatter( + "%(asctime)s - %(levelname)s [%(filename)s : %(funcName)s() : l. %(lineno)s]: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + if log_file: + log_file_path = Path(log_file) + log_file_path.parent.mkdir(parents=True, exist_ok=True) + file_handler = logging.FileHandler(log_file_path) + file_handler.setFormatter(logging_format) + logger.addHandler(file_handler) + else: + output_stdout = True + + if output_stdout: + stream = logging.StreamHandler() + stream.setFormatter(logging_format) + logger.addHandler(stream) + + +def create_optimizer(name: str, model: Module, lr: float, momentum: float) -> Optimizer: + """creates the specified optimizer with the given parameters + + Args: + name (str): str name of optimizer + model (Module): the model used for training + lr (float): learning rate + momentum (float): momentum (only for sgd optimizer) + + Raises: + ValueError: thrown if optimizer name not known + + Returns: + Optimizer: the model optimizer + """ + name = name.lower() + if name == "adam": + return Adam(params=model.parameters(), lr=lr) + elif name == "sgd": + return SGD(params=model.parameters(), lr=lr, momentum=momentum) + elif name == "radam": + return RAdam(params=model.parameters(), lr=lr) + else: + raise ValueError(f"No optimizer with name {name} found!") + + +def create_scheduler( + scheduler_name: Optional[str], + optimizer: Optimizer, + lr_factor: float, + lr_steps: Optional[list], + epochs: int, +) -> Union[_LRScheduler, None]: + """creates a learning rate scheduler with the given parameters + + Args: + scheduler_name (Optional[str]): str name of scheduler or None, in which case None will be returned + optimizer (Optimizer): the learning optimizer + lr_factor (float): the learning rate factor + lr_steps (Optional[list]): learning rate steps for the scheduler to take (only supported for step scheduler) + epochs (int): number of scheduler epochs (only supported for cosine scheduler) + + Raises: + ValueError: thrown if step scheduler was chosen but no steps were passed + ValueError: thrown if scheduler name not known and not None + + Returns: + Union[_LRScheduler, None]: either the learning rate scheduler object or None if scheduler_name was None + """ + if scheduler_name == "step": + if not lr_steps: + raise ValueError("step scheduler chosen but no lr steps passed!") + return MultiStepLR(optimizer, lr_steps, lr_factor) + elif scheduler_name == "exponential": + return ExponentialLR(optimizer, lr_factor) + elif scheduler_name == "cosine": + return CosineAnnealingLR(optimizer, epochs) + elif not scheduler_name: + return None + else: + raise ValueError(f"no scheduler with name {scheduler_name} found!") From 8a9d4da581b2822098615a186767513c463a803c Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 15 Jul 2022 15:04:31 +0200 Subject: [PATCH 095/208] set more reasonable defaults --- bitorch/models/dlrm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 974d9ac..3e2d8bb 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -164,12 +164,12 @@ def add_argparse_arguments(parent_parser: ArgumentParser): choices=[ Interaction_Operation_Type.CONCAT.value, Interaction_Operation_Type.PRODUCT.value], - default=Interaction_Operation_Type.CONCAT.value) + default=Interaction_Operation_Type.PRODUCT.value) parser.add_argument("--dense-embeddings", action="store_false", help="Disable sparse embeddings") - parser.add_argument("--binary-embedding", action="store_true", default=False, + parser.add_argument("--binary-embedding", action="store_true", default=True, help="toggles use of binary embeddings in model.") - parser.add_argument("--binary-top-mlp", action="store_true", default=False, + parser.add_argument("--binary-top-mlp", action="store_true", default=True, help="toggles use of binary top mlp in model.") parser.add_argument("--binary-bottom-mlp", action="store_true", default=False, help="toggles use of binary bottom mlp in model.") From 7deabf1fac814ab393a44eb1cd41f71d4a4976f1 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 15 Jul 2022 15:08:51 +0200 Subject: [PATCH 096/208] removed print statements --- bitorch/models/dlrm.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 974d9ac..5f5eb19 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -48,7 +48,6 @@ def create_mlp( for layer_size in layer_sizes[1:]: output_size = layer_size - print(input_size, output_size) mlp_layers.append(BatchNorm1d(input_size)) mlp_layers.append( QLinear(input_size, output_size, bias=False) if quantized else @@ -138,8 +137,6 @@ def __init__( elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: top_mlp_layer_sizes = [ embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] - print(bottom_mlp_layer_sizes) - print(top_mlp_layer_sizes) self.bottom_mlp = create_mlp( bottom_mlp_layer_sizes, quantized=binary_bottom_mlp, From 329de0f8ec303b63deb8439a779d24c99e4a5728 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Sat, 16 Jul 2022 20:24:48 +0200 Subject: [PATCH 097/208] add densenet model --- bitorch/models/__init__.py | 12 ++ bitorch/models/common_layers.py | 2 + bitorch/models/densenet.py | 339 ++++++++++++++++++++++++++++++++ tests/models/test_models.py | 8 + 4 files changed, 361 insertions(+) create mode 100644 bitorch/models/densenet.py diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index dd2e1d0..3d93f56 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -20,6 +20,13 @@ Resnet50V1, Resnet50V2, ) +from .densenet import ( + DenseNet, + DenseNet28, + DenseNet37, + DenseNet45, + DenseNetFlex, +) from .resnet_e import ( ResnetE, ResnetE18, @@ -42,6 +49,11 @@ "ResnetE", "ResnetE18", "ResnetE34", + "DenseNet", + "DenseNet28", + "DenseNet37", + "DenseNet45", + "DenseNetFlex", ] diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index a4c938e..97cb129 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -5,6 +5,8 @@ def get_initial_layers(variant: str, input_channels: int, output_channels: int) -> List[nn.Module]: """Get commonly used layers to extract initial features from the image.""" layers: List[nn.Module] = [] + if variant == "thumbnail": + layers.append(nn.Conv2D(input_channels, kernel_size=3, strides=1, padding=1, use_bias=False)) if variant == "imagenet": layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False)) layers.append(nn.BatchNorm2d(output_channels, momentum=0.9)) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py new file mode 100644 index 0000000..7afd868 --- /dev/null +++ b/bitorch/models/densenet.py @@ -0,0 +1,339 @@ +import logging +import argparse +from typing import Any, List, Optional + +import torch +from torch import nn +from torch.nn import Module, ChannelShuffle + +from .base import Model +from bitorch.layers import QConv2d +from bitorch.models.common_layers import get_initial_layers +from bitorch.datasets.base import BasicDataset + + +class DenseLayer(Module): + def __init__(self, num_features: int, growth_rate: int, bn_size: int, dilation: int, dropout: float): + super(DenseLayer, self).__init__() + self.dropout = dropout + self.num_features = num_features + self.features = nn.Sequential() + if bn_size == 0: + # no bottleneck + self._add_conv_block( + QConv2d(self.num_features, growth_rate, kernel_size=3, padding=dilation, dilation=dilation) + ) + else: + self._add_conv_block(QConv2d(self.num_features, bn_size * growth_rate, kernel_size=1)) + self._add_conv_block(QConv2d(bn_size * growth_rate, growth_rate, kernel_size=3, padding=1)) + + def _add_conv_block(self, layer: Module) -> None: + self.features.append(nn.BatchNorm2d(self.num_features)) + self.features.append(layer) + if self.dropout: + self.features.append(nn.Dropout(self.dropout)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + ident = x + x = self.features(x) + x = torch.cat([ident, x], dim=1) + return x + + +class BaseNetDense(Module): + """Densenet-BC model from the + `"Densely Connected Convolutional Networks" `_ paper. + """ + + def __init__( + self, + num_init_features: int, + growth_rate: int, + block_config: List[int], + reduction: List[float], + bn_size: int, + downsample: str, + initial_layers: str = "imagenet", + dropout: float = 0, + classes: int = 1000, + image_channels: int = 3, + dilated: bool = False, + ): + super(BaseNetDense, self).__init__() + self.num_blocks = len(block_config) + self.dilation = (1, 1, 2, 4) if dilated else (1, 1, 1, 1) + self.downsample_struct = downsample + self.bn_size = bn_size + self.growth_rate = growth_rate + self.dropout = dropout + self.reduction_rates = reduction + self.num_features = num_init_features + + self.features = nn.Sequential(*get_initial_layers(initial_layers, image_channels, self.num_features)) + # Add dense blocks + for i, repeat_num in enumerate(block_config): + self._make_repeated_base_blocks(repeat_num, i) + if i != len(block_config) - 1: + self._make_transition(i) + self.finalize = nn.Sequential( + nn.BatchNorm2d(self.num_features), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten() + ) + self.output = nn.Linear(self.num_features, classes) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.features(x) + x = self.finalize(x) + x = self.output(x) + return x + + def _add_base_block_structure(self, layer_num: int, dilation: int) -> None: + raise NotImplementedError() + + def _make_repeated_base_blocks(self, num_base_blocks: int, stage_index: int) -> None: + dilation = self.dilation[stage_index] + self.current_dense_block = nn.Sequential() + for i in range(num_base_blocks): + self._add_base_block_structure(i, dilation) + self.features.add_module("DenseBlock_%d" % (stage_index + 1), self.current_dense_block) + + def _add_dense_layer(self, layer_num: int, dilation: int) -> None: + dense_layer = DenseLayer(self.num_features, self.growth_rate, self.bn_size, dilation, self.dropout) + self.num_features += self.growth_rate + self.current_dense_block.add_module("DenseLayer_%d" % (layer_num + 1), dense_layer) + + def _make_transition(self, transition_num: int) -> None: + dilation = self.dilation[transition_num + 1] + num_out_features = self.num_features // self.reduction_rates[transition_num] + num_out_features = int(round(num_out_features / 32)) * 32 + + transition = nn.Sequential() + + for layer in self.downsample_struct.split(","): + if layer == "bn": + transition.append(nn.BatchNorm2d(self.num_features)) + elif layer == "relu": + transition.append(nn.ReLU()) + elif layer == "q_conv": + transition.append(QConv2d(self.num_features, num_out_features, kernel_size=1)) + elif "fp_conv" in layer: + groups = 1 + if ":" in layer: + groups = int(layer.split(":")[1]) + transition.append( + nn.Conv2d(self.num_features, num_out_features, kernel_size=1, groups=groups, bias=False) + ) + elif layer == "pool" and dilation == 1: + transition.append(nn.AvgPool2d(2, stride=2)) + elif layer == "max_pool" and dilation == 1: + transition.append(nn.MaxPool2d(2, stride=2)) + elif "cs" in layer: + groups = 16 + if ":" in layer: + groups = int(layer.split(":")[1]) + transition.append(ChannelShuffle(groups)) + + self.features.add_module("Transition_%d" % (transition_num + 1), transition) + self.num_features = num_out_features + + +class SpecificDenseNet(BaseNetDense): + def _add_base_block_structure(self, layer_num: int, dilation: int) -> None: + self._add_dense_layer(layer_num, dilation) + + +""" +DenseNet specifications +""" + +DOWNSAMPLE_STRUCT = "bn,max_pool,relu,fp_conv" + + +class DenseNet(Model): + name = "densenet" + densenet_spec = { + # block_config, reduction_factor, downsampling + None: (None, [1 / 2, 1 / 2, 1 / 2], DOWNSAMPLE_STRUCT), + 28: ([6, 6, 6, 5], [1 / 2.7, 1 / 2.7, 1 / 2.2], DOWNSAMPLE_STRUCT), + 37: ([6, 8, 12, 6], [1 / 3.3, 1 / 3.3, 1 / 4], DOWNSAMPLE_STRUCT), + 45: ([6, 12, 14, 8], [1 / 2.7, 1 / 3.3, 1 / 4], DOWNSAMPLE_STRUCT), + } + + def __init__( + self, + num_layers: Optional[int], + dataset: BasicDataset, + num_init_features: int = 64, + growth_rate: int = 64, + bn_size: int = 0, + dropout: float = 0, + dilated: bool = False, + flex_block_config: List[int] = None, + ) -> None: + super(DenseNet, self).__init__(dataset) + self._model = self.create_densenet( + num_layers, + num_init_features, + growth_rate, + bn_size, + dropout, + dilated, + flex_block_config, + self._dataset.num_classes, + self._dataset.name, + self._dataset.shape[1], + ) + logging.info(f"building DenseNet with {str(num_layers)} layers...") + + def create_densenet( + self, + num_layers: Optional[int], + num_init_features: int, + growth_rate: int, + bn_size: int, + dropout: float, + dilated: bool, + flex_block_config: Optional[List[int]], + classes: int = 1000, + initial_layers: str = "imagenet", + image_channels: int = 3, + ) -> Module: + """Creates a densenet complying to given version and layer number. + + Args: + num_layers (int): number of layers to be build. + num_init_features (int, optional): number of initial features. + growth_rate (int, optional): growth rate of the channels. + bn_size (int, optional): size of the bottleneck. + dropout (float, optional): dropout percentage in dense layers. + dilated (bool, optional): whether to use dilation in convolutions. + flex_block_config (List[int], optional) number of blocks in a flex model. + classes (int, optional): number of output classes. Defaults to 1000. + initial_layers (str, optional): name of set of initial layers to be used. Defaults to "imagenet". + image_channels (int, optional): number of channels of input images. Defaults to 3. + + Raises: + ValueError: raised if no densenet specification for given num_layers is listed in the densenet_spec dict, + block config is not given as a list of ints, + number of reductions is incorrect + + Returns: + Module: densenet model + """ + if num_layers not in self.densenet_spec: + raise ValueError(f"No DenseNet spec for {num_layers} available!") + + block_config, reduction_factor, downsampling = self.densenet_spec[num_layers] + + if num_layers is None and flex_block_config is not None: + block_config = flex_block_config + + reduction = [1 / x for x in reduction_factor] + if not isinstance(block_config, List): + raise ValueError(f"block config {block_config} must be a list") + if not len(reduction) == len(block_config) - 1: + raise ValueError(f'"wrong number of reductions, should be {len(block_config) - 1}"') + + return SpecificDenseNet( + num_init_features, + growth_rate, + block_config, + reduction, + bn_size, + downsampling, + initial_layers, + dropout, + classes, + image_channels, + dilated, + ) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--densenet-num-layers", + type=int, + choices=[None, 28, 37, 45], + required=True, + help="number of layers to be used inside densenet", + ) + parser.add_argument( + "--reduction", + type=str, + required=False, + help='divide channels by this number in transition blocks (3 values, e.g. "2,2.5,3")', + ) + parser.add_argument("--growth-rate", type=int, required=False, help="add this many features each block") + parser.add_argument( + "--init-features", type=int, required=False, help="start with this many filters in the first layer" + ) + parser.add_argument( + "--downsample-structure", + type=str, + required=False, + help="layers in downsampling branch (available: bn,relu,conv,fp_conv,pool,max_pool)", + ) + + +class DenseNetFlex(DenseNet): + """DenseNet-Flex model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" + ` paper. + """ + + name = "densenetflex" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(DenseNetFlex, self).__init__(None, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--block-config", + type=str, + required=True, + help="how many blocks to use in a flex model", + ) + + +class DenseNet28(DenseNet): + """DenseNet-28 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" + ` paper. + """ + + name = "densenet28" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(DenseNet28, self).__init__(28, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class DenseNet37(DenseNet): + """DenseNet-37 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" + ` paper. + """ + + name = "densenet37" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(DenseNet37, self).__init__(37, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class DenseNet45(DenseNet): + """DenseNet-45 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" + ` paper. + """ + + name = "densenet45" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(DenseNet45, self).__init__(45, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 0a89a81..d8b8e33 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -13,6 +13,10 @@ ResnetE, ResnetE18, ResnetE34, + DenseNet28, + DenseNet37, + DenseNet45, + DenseNetFlex, ) import torch import numpy as np @@ -30,6 +34,10 @@ [Resnet18V2, {}, ALL_DATASETS], [Resnet34V2, {}, ALL_DATASETS], [Resnet50V2, {}, ALL_DATASETS], + [DenseNet28, {}, ALL_DATASETS], + [DenseNet37, {}, ALL_DATASETS], + [DenseNet45, {}, ALL_DATASETS], + [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_DATASETS], [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], [ResnetE18, {}, RGB_DATASETS], [ResnetE34, {}, RGB_DATASETS], From de5c72020bff9df4346a6d8bf110ecbb884fa5e7 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Sun, 17 Jul 2022 17:19:09 +0200 Subject: [PATCH 098/208] add meliusnet and basedensenet constructor --- bitorch/models/__init__.py | 20 +++ bitorch/models/densenet.py | 138 +++++++++---------- bitorch/models/meliusnet.py | 255 ++++++++++++++++++++++++++++++++++++ tests/models/test_models.py | 16 +++ 4 files changed, 363 insertions(+), 66 deletions(-) create mode 100644 bitorch/models/meliusnet.py diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index 3d93f56..ec12a0f 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -27,6 +27,17 @@ DenseNet45, DenseNetFlex, ) +from .meliusnet import ( + MeliusNet, + MeliusNet22, + MeliusNet23, + MeliusNet42, + MeliusNet59, + MeliusNeta, + MeliusNetb, + MeliusNetc, + MeliusNetFlex, +) from .resnet_e import ( ResnetE, ResnetE18, @@ -54,6 +65,15 @@ "DenseNet37", "DenseNet45", "DenseNetFlex", + "MeliusNet", + "MeliusNet22", + "MeliusNet23", + "MeliusNet42", + "MeliusNet59", + "MeliusNeta", + "MeliusNetb", + "MeliusNetc", + "MeliusNetFlex", ] diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 7afd868..a05c8c8 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -1,6 +1,6 @@ import logging import argparse -from typing import Any, List, Optional +from typing import Any, List, Optional, Type, Union import torch from torch import nn @@ -136,11 +136,78 @@ def _make_transition(self, transition_num: int) -> None: self.num_features = num_out_features -class SpecificDenseNet(BaseNetDense): +class _DenseNet(BaseNetDense): def _add_base_block_structure(self, layer_num: int, dilation: int) -> None: self._add_dense_layer(layer_num, dilation) +def basedensenet_constructor( + spec: dict, + model: Type[BaseNetDense], + num_layers: Optional[Union[int, str]], + num_init_features: int, + growth_rate: int, + bn_size: int, + dropout: float, + dilated: bool, + flex_block_config: Optional[List[int]], + classes: int = 1000, + initial_layers: str = "imagenet", + image_channels: int = 3, +) -> Module: + """Creates a densenet of the given model type with given layer numbers. + + Args: + spec (dict): specification that holds block config, reduction factors and downsample layer names + model (Type[BaseNetDense]): the model to instantiate. + num_layers (int): number of layers to be build. + num_init_features (int, optional): number of initial features. + growth_rate (int, optional): growth rate of the channels. + bn_size (int, optional): size of the bottleneck. + dropout (float, optional): dropout percentage in dense layers. + dilated (bool, optional): whether to use dilation in convolutions. + flex_block_config (List[int], optional) number of blocks in a flex model. + classes (int, optional): number of output classes. Defaults to 1000. + initial_layers (str, optional): name of set of initial layers to be used. Defaults to "imagenet". + image_channels (int, optional): number of channels of input images. Defaults to 3. + + Raises: + ValueError: raised if no specification for given num_layers is listed in the given spec dict, + block config is not given as a list of ints, + number of reductions is incorrect + + Returns: + Module: instance of model + """ + if num_layers not in spec: + raise ValueError(f"No DenseNet spec for {num_layers} available!") + + block_config, reduction_factor, downsampling = spec[num_layers] + + if num_layers is None and flex_block_config is not None: + block_config = flex_block_config + + reduction = [1 / x for x in reduction_factor] + if not isinstance(block_config, List): + raise ValueError(f"block config {block_config} must be a list") + if not len(reduction) == len(block_config) - 1: + raise ValueError(f'"wrong number of reductions, should be {len(block_config) - 1}"') + + return model( + num_init_features, + growth_rate, + block_config, + reduction, + bn_size, + downsampling, + initial_layers, + dropout, + classes, + image_channels, + dilated, + ) + + """ DenseNet specifications """ @@ -170,7 +237,9 @@ def __init__( flex_block_config: List[int] = None, ) -> None: super(DenseNet, self).__init__(dataset) - self._model = self.create_densenet( + self._model = basedensenet_constructor( + self.densenet_spec, + _DenseNet, num_layers, num_init_features, growth_rate, @@ -184,69 +253,6 @@ def __init__( ) logging.info(f"building DenseNet with {str(num_layers)} layers...") - def create_densenet( - self, - num_layers: Optional[int], - num_init_features: int, - growth_rate: int, - bn_size: int, - dropout: float, - dilated: bool, - flex_block_config: Optional[List[int]], - classes: int = 1000, - initial_layers: str = "imagenet", - image_channels: int = 3, - ) -> Module: - """Creates a densenet complying to given version and layer number. - - Args: - num_layers (int): number of layers to be build. - num_init_features (int, optional): number of initial features. - growth_rate (int, optional): growth rate of the channels. - bn_size (int, optional): size of the bottleneck. - dropout (float, optional): dropout percentage in dense layers. - dilated (bool, optional): whether to use dilation in convolutions. - flex_block_config (List[int], optional) number of blocks in a flex model. - classes (int, optional): number of output classes. Defaults to 1000. - initial_layers (str, optional): name of set of initial layers to be used. Defaults to "imagenet". - image_channels (int, optional): number of channels of input images. Defaults to 3. - - Raises: - ValueError: raised if no densenet specification for given num_layers is listed in the densenet_spec dict, - block config is not given as a list of ints, - number of reductions is incorrect - - Returns: - Module: densenet model - """ - if num_layers not in self.densenet_spec: - raise ValueError(f"No DenseNet spec for {num_layers} available!") - - block_config, reduction_factor, downsampling = self.densenet_spec[num_layers] - - if num_layers is None and flex_block_config is not None: - block_config = flex_block_config - - reduction = [1 / x for x in reduction_factor] - if not isinstance(block_config, List): - raise ValueError(f"block config {block_config} must be a list") - if not len(reduction) == len(block_config) - 1: - raise ValueError(f'"wrong number of reductions, should be {len(block_config) - 1}"') - - return SpecificDenseNet( - num_init_features, - growth_rate, - block_config, - reduction, - bn_size, - downsampling, - initial_layers, - dropout, - classes, - image_channels, - dilated, - ) - @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py new file mode 100644 index 0000000..b9a9a10 --- /dev/null +++ b/bitorch/models/meliusnet.py @@ -0,0 +1,255 @@ +from .densenet import * + + +# Blocks +class ImprovementBlock(Module): + """ImprovementBlock which improves the last n channels""" + + def __init__(self, channels: int, in_channels: int, dilation: int = 1): + super(ImprovementBlock, self).__init__() + self.body = nn.Sequential() + self.body.append(nn.BatchNorm2d(in_channels)) + self.body.append(QConv2d(in_channels, channels, kernel_size=3, stride=1, padding=dilation, dilation=dilation)) + + self.use_sliced_addition = channels != in_channels + if self.use_sliced_addition: + assert channels < in_channels + self.slices = [0, in_channels - channels, in_channels] + self.slices_add_x = [False, True] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x = self.body(x) + if not self.use_sliced_addition: + return x + residual + + parts = [] + for add_x, slice_begin, slice_end in zip(self.slices_add_x, self.slices[:-1], self.slices[1:]): + length = slice_end - slice_begin + if length is 0: + continue + result = torch.narrow(residual, dim=1, start=slice_begin, length=length) + if add_x: + result = result + x + parts.append(result) + return torch.cat(parts, dim=1) + + +class _MeliusNet(BaseNetDense): + def _add_base_block_structure(self, layer_num: int, dilation: int) -> None: + self._add_dense_layer(layer_num, dilation) + self.current_dense_block.add_module( + "ImprovementBlock%d" % (layer_num + 1), + ImprovementBlock(self.growth_rate, self.num_features, dilation=dilation), + ) + + +class MeliusNet(Model): + name = "meliusnet" + + meliusnet_spec = { + # name: block_config, reduction_factors, downsampling + None: (None, [1 / 2, 1 / 2, 1 / 2], DOWNSAMPLE_STRUCT), + 23: ([2, 4, 6, 6], [128 / 192, 192 / 384, 288 / 576], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:8")), + 22: ([4, 5, 4, 4], [160 / 320, 224 / 480, 256 / 480], DOWNSAMPLE_STRUCT), + 29: ([4, 6, 8, 6], [128 / 320, 192 / 512, 256 / 704], DOWNSAMPLE_STRUCT), + 42: ([5, 8, 14, 10], [160 / 384, 256 / 672, 416 / 1152], DOWNSAMPLE_STRUCT), + 59: ([6, 12, 24, 12], [192 / 448, 320 / 960, 544 / 1856], DOWNSAMPLE_STRUCT), + "a": ([4, 5, 5, 6], [160 / 320, 256 / 480, 288 / 576], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:4")), + "b": ([4, 6, 8, 6], [160 / 320, 224 / 544, 320 / 736], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:2")), + "c": ([3, 5, 10, 6], [128 / 256, 192 / 448, 288 / 832], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:4")), + } + + def __init__( + self, + num_layers: Optional[Union[int, str]], + dataset: BasicDataset, + num_init_features: int = 64, + growth_rate: int = 64, + bn_size: int = 0, + dropout: float = 0, + dilated: bool = False, + flex_block_config: List[int] = None, + ) -> None: + super(MeliusNet, self).__init__(dataset) + self._model = basedensenet_constructor( + self.meliusnet_spec, + _MeliusNet, + num_layers, + num_init_features, + growth_rate, + bn_size, + dropout, + dilated, + flex_block_config, + self._dataset.num_classes, + self._dataset.name, + self._dataset.shape[1], + ) + logging.info(f"building DenseNet with {str(num_layers)} layers...") + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--densenet-num-layers", + type=int, + choices=[None, 28, 37, 45], + required=True, + help="number of layers to be used inside densenet", + ) + parser.add_argument( + "--reduction", + type=str, + required=False, + help='divide channels by this number in transition blocks (3 values, e.g. "2,2.5,3")', + ) + parser.add_argument("--growth-rate", type=int, required=False, help="add this many features each block") + parser.add_argument( + "--init-features", type=int, required=False, help="start with this many filters in the first layer" + ) + parser.add_argument( + "--downsample-structure", + type=str, + required=False, + help="layers in downsampling branch (available: bn,relu,conv,fp_conv,pool,max_pool)", + ) + + +class MeliusNetFlex(DenseNet): + """DenseNet-Flex model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy??" + ` paper. + """ + + name = "meliusnetflex" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNetFlex, self).__init__(None, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--block-config", + type=str, + required=True, + help="how many blocks to use in a flex model", + ) + + +class MeliusNet22(MeliusNet): + """MeliusNet-22 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnet22" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNet22, self).__init__(22, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNet23(MeliusNet): + """MeliusNet-23 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnet23" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNet23, self).__init__(23, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNet29(MeliusNet): + """MeliusNet-29 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnet29" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNet29, self).__init__(29, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNet42(MeliusNet): + """MeliusNet-22 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnet42" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNet42, self).__init__(42, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNet59(MeliusNet): + """MeliusNet-59 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnet59" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNet59, self).__init__(59, *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNeta(MeliusNet): + """MeliusNet-a model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusneta" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNeta, self).__init__("a", *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNetb(MeliusNet): + """MeliusNet-b model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnetb" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNetb, self).__init__("b", *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass + + +class MeliusNetc(MeliusNet): + """MeliusNet-c model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + ` paper. + """ + + name = "meliusnetc" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(MeliusNetc, self).__init__("c", *args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + pass diff --git a/tests/models/test_models.py b/tests/models/test_models.py index d8b8e33..1645a48 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -17,6 +17,14 @@ DenseNet37, DenseNet45, DenseNetFlex, + MeliusNet22, + MeliusNet42, + MeliusNetFlex, + MeliusNet23, + MeliusNet59, + MeliusNeta, + MeliusNetb, + MeliusNetc, ) import torch import numpy as np @@ -38,6 +46,14 @@ [DenseNet37, {}, ALL_DATASETS], [DenseNet45, {}, ALL_DATASETS], [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_DATASETS], + [MeliusNet22, {}, ALL_DATASETS], + [MeliusNet23, {}, ALL_DATASETS], + [MeliusNet42, {}, ALL_DATASETS], + [MeliusNet59, {}, ALL_DATASETS], + [MeliusNeta, {}, ALL_DATASETS], + [MeliusNetb, {}, ALL_DATASETS], + [MeliusNetc, {}, ALL_DATASETS], + [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_DATASETS], [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], [ResnetE18, {}, RGB_DATASETS], [ResnetE34, {}, RGB_DATASETS], From 95d56236238221f37d699f695318f547b5d1db31 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Sun, 17 Jul 2022 17:54:23 +0200 Subject: [PATCH 099/208] fix codestyle --- bitorch/models/meliusnet.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index b9a9a10..1a89afa 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -1,4 +1,15 @@ -from .densenet import * +import argparse +import logging +from typing import Optional, Union, List, Any + +import torch +from torch import nn +from torch.nn import Module + +from .densenet import BaseNetDense, DenseNet, DOWNSAMPLE_STRUCT, basedensenet_constructor +from .base import Model +from bitorch.layers import QConv2d +from bitorch.datasets import BasicDataset # Blocks @@ -26,7 +37,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: parts = [] for add_x, slice_begin, slice_end in zip(self.slices_add_x, self.slices[:-1], self.slices[1:]): length = slice_end - slice_begin - if length is 0: + if length == 0: continue result = torch.narrow(residual, dim=1, start=slice_begin, length=length) if add_x: From 4a1b958d8d1095c5573c09f487169b3bf628682f Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Mon, 18 Jul 2022 21:34:15 +0200 Subject: [PATCH 100/208] edit docu and small fixes --- bitorch/models/densenet.py | 2 +- bitorch/models/meliusnet.py | 42 ++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index a05c8c8..3c81ffa 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -180,7 +180,7 @@ def basedensenet_constructor( Module: instance of model """ if num_layers not in spec: - raise ValueError(f"No DenseNet spec for {num_layers} available!") + raise ValueError(f"No spec for {num_layers} available!") block_config, reduction_factor, downsampling = spec[num_layers] diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 1a89afa..119a1ab 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -1,12 +1,12 @@ import argparse import logging -from typing import Optional, Union, List, Any +from typing import Optional, List, Any import torch from torch import nn from torch.nn import Module -from .densenet import BaseNetDense, DenseNet, DOWNSAMPLE_STRUCT, basedensenet_constructor +from .densenet import BaseNetDense, DOWNSAMPLE_STRUCT, basedensenet_constructor from .base import Model from bitorch.layers import QConv2d from bitorch.datasets import BasicDataset @@ -61,11 +61,11 @@ class MeliusNet(Model): meliusnet_spec = { # name: block_config, reduction_factors, downsampling None: (None, [1 / 2, 1 / 2, 1 / 2], DOWNSAMPLE_STRUCT), - 23: ([2, 4, 6, 6], [128 / 192, 192 / 384, 288 / 576], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:8")), - 22: ([4, 5, 4, 4], [160 / 320, 224 / 480, 256 / 480], DOWNSAMPLE_STRUCT), - 29: ([4, 6, 8, 6], [128 / 320, 192 / 512, 256 / 704], DOWNSAMPLE_STRUCT), - 42: ([5, 8, 14, 10], [160 / 384, 256 / 672, 416 / 1152], DOWNSAMPLE_STRUCT), - 59: ([6, 12, 24, 12], [192 / 448, 320 / 960, 544 / 1856], DOWNSAMPLE_STRUCT), + "23": ([2, 4, 6, 6], [128 / 192, 192 / 384, 288 / 576], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:8")), + "22": ([4, 5, 4, 4], [160 / 320, 224 / 480, 256 / 480], DOWNSAMPLE_STRUCT), + "29": ([4, 6, 8, 6], [128 / 320, 192 / 512, 256 / 704], DOWNSAMPLE_STRUCT), + "42": ([5, 8, 14, 10], [160 / 384, 256 / 672, 416 / 1152], DOWNSAMPLE_STRUCT), + "59": ([6, 12, 24, 12], [192 / 448, 320 / 960, 544 / 1856], DOWNSAMPLE_STRUCT), "a": ([4, 5, 5, 6], [160 / 320, 256 / 480, 288 / 576], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:4")), "b": ([4, 6, 8, 6], [160 / 320, 224 / 544, 320 / 736], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:2")), "c": ([3, 5, 10, 6], [128 / 256, 192 / 448, 288 / 832], DOWNSAMPLE_STRUCT.replace("fp_conv", "cs,fp_conv:4")), @@ -73,7 +73,7 @@ class MeliusNet(Model): def __init__( self, - num_layers: Optional[Union[int, str]], + num_layers: Optional[str], dataset: BasicDataset, num_init_features: int = 64, growth_rate: int = 64, @@ -97,22 +97,22 @@ def __init__( self._dataset.name, self._dataset.shape[1], ) - logging.info(f"building DenseNet with {str(num_layers)} layers...") + logging.info(f"building MeliusNet with {str(num_layers)} layers...") @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--densenet-num-layers", - type=int, - choices=[None, 28, 37, 45], + "--melius-num-layers", + type=str, + choices=[None, "22", "23", "29", "42", "59", "a", "b", "c"], required=True, - help="number of layers to be used inside densenet", + help="number of layers to be used inside meliusnet", ) parser.add_argument( "--reduction", type=str, required=False, - help='divide channels by this number in transition blocks (3 values, e.g. "2,2.5,3")', + help="divide channels by this number in transition blocks", ) parser.add_argument("--growth-rate", type=int, required=False, help="add this many features each block") parser.add_argument( @@ -126,8 +126,8 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) -class MeliusNetFlex(DenseNet): - """DenseNet-Flex model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy??" +class MeliusNetFlex(MeliusNet): + """MeliusNet-Flex model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -154,7 +154,7 @@ class MeliusNet22(MeliusNet): name = "meliusnet22" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNet22, self).__init__(22, *args, **kwargs) + super(MeliusNet22, self).__init__("22", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: @@ -169,7 +169,7 @@ class MeliusNet23(MeliusNet): name = "meliusnet23" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNet23, self).__init__(23, *args, **kwargs) + super(MeliusNet23, self).__init__("23", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: @@ -184,7 +184,7 @@ class MeliusNet29(MeliusNet): name = "meliusnet29" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNet29, self).__init__(29, *args, **kwargs) + super(MeliusNet29, self).__init__("29", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: @@ -199,7 +199,7 @@ class MeliusNet42(MeliusNet): name = "meliusnet42" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNet42, self).__init__(42, *args, **kwargs) + super(MeliusNet42, self).__init__("42", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: @@ -214,7 +214,7 @@ class MeliusNet59(MeliusNet): name = "meliusnet59" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNet59, self).__init__(59, *args, **kwargs) + super(MeliusNet59, self).__init__("59", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: From deb29cd99bbdeb9d94b6d2a31332da07cd7ad320 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 22 Jul 2022 18:17:56 +0200 Subject: [PATCH 101/208] use CamelCase and update changelog --- CHANGELOG.md | 3 +++ bitorch/models/__init__.py | 12 ++++++------ bitorch/models/densenet.py | 39 +++++++++++++++++++++++++++---------- bitorch/models/meliusnet.py | 25 ++++++++++++++++-------- tests/models/test_models.py | 12 ++++++------ 5 files changed, 61 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae9b4b..4274979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- new models: + - [MeliusNet](bitorch/models/meliusnet.py) + - [BinaryDenseNet](bitorch/models/densenet.py) - simple example script for MNIST - support for integration of bitorch's inference engine for the following layers - QLinear diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index ec12a0f..160b815 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -33,9 +33,9 @@ MeliusNet23, MeliusNet42, MeliusNet59, - MeliusNeta, - MeliusNetb, - MeliusNetc, + MeliusNetA, + MeliusNetB, + MeliusNetC, MeliusNetFlex, ) from .resnet_e import ( @@ -70,9 +70,9 @@ "MeliusNet23", "MeliusNet42", "MeliusNet59", - "MeliusNeta", - "MeliusNetb", - "MeliusNetc", + "MeliusNetA", + "MeliusNetB", + "MeliusNetC", "MeliusNetFlex", ] diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 3c81ffa..5170c33 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -268,9 +268,17 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: required=False, help='divide channels by this number in transition blocks (3 values, e.g. "2,2.5,3")', ) - parser.add_argument("--growth-rate", type=int, required=False, help="add this many features each block") parser.add_argument( - "--init-features", type=int, required=False, help="start with this many filters in the first layer" + "--growth-rate", + type=int, + required=False, + help="add this many features each block", + ) + parser.add_argument( + "--init-features", + type=int, + required=False, + help="start with this many filters in the first layer", ) parser.add_argument( "--downsample-structure", @@ -281,8 +289,10 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class DenseNetFlex(DenseNet): - """DenseNet-Flex model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" - ` paper. + """DenseNet-Flex model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks" + ` paper. + + """ name = "densenetflex" @@ -301,8 +311,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class DenseNet28(DenseNet): - """DenseNet-28 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" - ` paper. + """ + DenseNet-28 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + + .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": + https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ name = "densenet28" @@ -316,8 +329,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class DenseNet37(DenseNet): - """DenseNet-37 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" - ` paper. + """ + DenseNet-37 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + + .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": + https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ name = "densenet37" @@ -331,8 +347,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class DenseNet45(DenseNet): - """DenseNet-45 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" - ` paper. + """ + DenseNet-45 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + + .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": + https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ name = "densenet45" diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 119a1ab..9fd56fa 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -114,9 +114,17 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: required=False, help="divide channels by this number in transition blocks", ) - parser.add_argument("--growth-rate", type=int, required=False, help="add this many features each block") parser.add_argument( - "--init-features", type=int, required=False, help="start with this many filters in the first layer" + "--growth-rate", + type=int, + required=False, + help="add this many features each block", + ) + parser.add_argument( + "--init-features", + type=int, + required=False, + help="start with this many filters in the first layer", ) parser.add_argument( "--downsample-structure", @@ -138,6 +146,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + super().add_argparse_arguments(parser) # type: ignore parser.add_argument( "--block-config", type=str, @@ -221,7 +230,7 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: pass -class MeliusNeta(MeliusNet): +class MeliusNetA(MeliusNet): """MeliusNet-a model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -229,14 +238,14 @@ class MeliusNeta(MeliusNet): name = "meliusneta" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNeta, self).__init__("a", *args, **kwargs) + super(MeliusNetA, self).__init__("a", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: pass -class MeliusNetb(MeliusNet): +class MeliusNetB(MeliusNet): """MeliusNet-b model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -244,14 +253,14 @@ class MeliusNetb(MeliusNet): name = "meliusnetb" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNetb, self).__init__("b", *args, **kwargs) + super(MeliusNetB, self).__init__("b", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: pass -class MeliusNetc(MeliusNet): +class MeliusNetC(MeliusNet): """MeliusNet-c model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -259,7 +268,7 @@ class MeliusNetc(MeliusNet): name = "meliusnetc" def __init__(self, *args: Any, **kwargs: Any) -> None: - super(MeliusNetc, self).__init__("c", *args, **kwargs) + super(MeliusNetC, self).__init__("c", *args, **kwargs) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 1645a48..ed1d06c 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -22,9 +22,9 @@ MeliusNetFlex, MeliusNet23, MeliusNet59, - MeliusNeta, - MeliusNetb, - MeliusNetc, + MeliusNetA, + MeliusNetB, + MeliusNetC, ) import torch import numpy as np @@ -50,9 +50,9 @@ [MeliusNet23, {}, ALL_DATASETS], [MeliusNet42, {}, ALL_DATASETS], [MeliusNet59, {}, ALL_DATASETS], - [MeliusNeta, {}, ALL_DATASETS], - [MeliusNetb, {}, ALL_DATASETS], - [MeliusNetc, {}, ALL_DATASETS], + [MeliusNetA, {}, ALL_DATASETS], + [MeliusNetB, {}, ALL_DATASETS], + [MeliusNetC, {}, ALL_DATASETS], [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_DATASETS], [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], [ResnetE18, {}, RGB_DATASETS], From 24dbfc0083284d9bd93d4399f19906eb2e647113 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 27 Jul 2022 09:58:04 +0200 Subject: [PATCH 102/208] correct version --- bitorch/models/meliusnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 9fd56fa..9c71738 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -201,7 +201,7 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class MeliusNet42(MeliusNet): - """MeliusNet-22 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + """MeliusNet-42 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ From 514794b507f26bd7f97e650754a494ea6d62b559 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Thu, 28 Jul 2022 20:17:00 +0200 Subject: [PATCH 103/208] made linters happy --- bitorch/layers/qembedding.py | 10 +-- bitorch/models/dlrm.py | 26 ++++--- examples/dlrm/criteo.py | 12 ++- .../dlrm/facebook_dataloading/data_utils.py | 71 +++++++++--------- .../facebook_dataloading/dataloading_fb.py | 75 ++++++------------- examples/dlrm/train_dlrm.py | 55 ++++---------- examples/dlrm/utils/lightning_model.py | 12 +-- 7 files changed, 101 insertions(+), 160 deletions(-) diff --git a/bitorch/layers/qembedding.py b/bitorch/layers/qembedding.py index 823b560..2f97f9e 100644 --- a/bitorch/layers/qembedding.py +++ b/bitorch/layers/qembedding.py @@ -1,4 +1,4 @@ -from typing import Union, Optional +from typing import Any, Union, Optional from torch import Tensor from torch.nn import EmbeddingBag, Embedding from torch.nn.functional import embedding_bag, embedding @@ -15,11 +15,11 @@ class QEmbeddingBag(EmbeddingBag): def __init__( self, - *args: int, + *args: Any, embedding_dim: int, weight_quantization: Union[Quantization, str] = None, output_quantization: Union[Quantization, str] = None, - **kwargs: int, + **kwargs: Any, ) -> None: super(QEmbeddingBag, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore """load quantization functions""" @@ -86,11 +86,11 @@ class QEmbedding(Embedding): def __init__( self, - *args: int, + *args: Any, embedding_dim: int, weight_quantization: Union[Quantization, str] = None, output_quantization: Union[Quantization, str] = None, - **kwargs: int, + **kwargs: Any, ) -> None: super(QEmbedding, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore """load quantization functions""" diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 13641bf..ab2782b 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -1,6 +1,6 @@ from argparse import ArgumentParser from enum import Enum -from typing import List, Tuple, Union +from typing import Any, List, Union import logging import torch from torch.nn import ( @@ -11,6 +11,7 @@ EmbeddingBag, ModuleList, BatchNorm1d, + Module ) import numpy as np from bitorch.datasets.base import BasicDataset @@ -21,7 +22,9 @@ from bitorch.layers.qembedding import QEmbeddingBag -def parse_layer_sizes(layer_sizes_str: str) -> List[int]: +def parse_layer_sizes(layer_sizes_str: Union[List[int], str]) -> List[int]: + if isinstance(layer_sizes_str, list): + return [int(size) for size in layer_sizes_str] layer_sizes_str = layer_sizes_str.replace('[', '').replace(']', '') return [int(size) for size in layer_sizes_str.split(",")] @@ -44,7 +47,7 @@ def create_mlp( all other layers will have relu activation. """ input_size = layer_sizes[0] - mlp_layers = [] + mlp_layers: List[Module] = [] for layer_size in layer_sizes[1:]: output_size = layer_size @@ -73,7 +76,7 @@ def create_embeddings( embedding_dimension: int, layer_sizes: List[int], quantized: bool, - sparse=False) -> Tuple[ModuleList, List[Union[None, torch.Tensor]]]: + sparse: bool = False) -> ModuleList: """creates the embedding layers for each category.""" if sparse: logging.info("USING SPARSE EMBEDDINGS") @@ -102,7 +105,7 @@ class DLRM(Model): name = "dlrm" total_size = 1.0 inference_speed = 1.0 - validation_results = [] + validation_results: List[dict] = [] def __init__( self, @@ -110,13 +113,13 @@ def __init__( dense_feature_size: int, embedding_dimension: int, embedding_layer_sizes: List[int], - bottom_mlp_layer_sizes: List[int], - top_mlp_layer_sizes: List[int], + bottom_mlp_layer_sizes: Union[List[int], str], + top_mlp_layer_sizes: Union[List[int], str], interaction_operation: Interaction_Operation_Type, binary_bottom_mlp: bool, binary_top_mlp: bool, binary_embedding: bool, - **kwargs) -> None: + **kwargs: Any) -> None: super().__init__(dataset) self.interaction_operation = interaction_operation self.embedding_layers = create_embeddings( @@ -148,7 +151,7 @@ def __init__( self.top_mlp[-1] = Sigmoid() @staticmethod - def add_argparse_arguments(parent_parser: ArgumentParser): + def add_argparse_arguments(parent_parser: ArgumentParser) -> None: parser = parent_parser.add_argument_group("DLRM Model") parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64]", help="layer sizes of the bottom mlp") @@ -170,7 +173,6 @@ def add_argparse_arguments(parent_parser: ArgumentParser): help="toggles use of binary top mlp in model.") parser.add_argument("--binary-bottom-mlp", action="store_true", default=False, help="toggles use of binary bottom mlp in model.") - return parent_parser def forward_embeddings(self, categorical_values_i: torch.Tensor, categorical_values_o: torch.Tensor) -> List[torch.Tensor]: @@ -182,7 +184,7 @@ def forward_embeddings(self, categorical_values_i: torch.Tensor, embedding_outputs.append(embedding_layer(index_group, offset_group)) return embedding_outputs - def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[torch.Tensor]): + def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[torch.Tensor]) -> torch.Tensor: if self.interaction_operation == Interaction_Operation_Type.PRODUCT.value: batch_size, dimension = mlp_output.shape concated_values = torch.cat([mlp_output] + embedding_outputs, dim=1).view((batch_size, -1, dimension)) @@ -199,7 +201,7 @@ def feature_interaction(self, mlp_output: torch.Tensor, embedding_outputs: List[ return result - def forward(self, dense_values, categorical_values): + def forward(self, dense_values: torch.Tensor, categorical_values: torch.Tensor) -> torch.Tensor: # type: ignore mlp_output = self.bottom_mlp(dense_values) embedding_outputs = self.forward_embeddings(*categorical_values) feature_interactions = self.feature_interaction(mlp_output, embedding_outputs) diff --git a/examples/dlrm/criteo.py b/examples/dlrm/criteo.py index fdc429b..fb7e498 100644 --- a/examples/dlrm/criteo.py +++ b/examples/dlrm/criteo.py @@ -1,6 +1,8 @@ import gc +from typing import Tuple from torch.utils.data import Dataset import logging +import torch import os import numpy as np from bitorch.datasets.base import BasicDataset @@ -9,21 +11,17 @@ class SplitCriteoDataset(Dataset): """Dataset to get items from a dataset for each split. Useful if dataset creation takes a lot of time and can be done exactly once.""" - def __init__(self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0): + def __init__(self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0) -> None: self.dataset = dataset - - # split_index = int(train_split_fraction * len(dataset)) - # self.indices = list(range(len(dataset))) - # self.indices = self.indices[:split_index] if split == "train" else self.indices[split_index:] self.indices = self.dataset.train_indices if split == "train" else self.dataset.test_indices dataset_size = int(len(self.indices) * (1.0 - ignore_size)) self.indices = np.random.choice(self.indices, size=dataset_size, replace=False) - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: return self.dataset[self.indices[idx]] - def __len__(self): + def __len__(self) -> int: return len(self.indices) diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py index 84556f5..93c177c 100644 --- a/examples/dlrm/facebook_dataloading/data_utils.py +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -1,3 +1,4 @@ +# type: ignore # Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the @@ -43,6 +44,7 @@ import logging from os import path from multiprocessing import Process, Manager +from typing import Any, Union # import io # from io import StringIO # import collections as coll @@ -50,7 +52,7 @@ import numpy as np -def processCriteoAdData(d_path, d_file, npzfile, i, convertDicts, pre_comp_counts): +def processCriteoAdData(d_path: str, d_file: str, npzfile: str, i: int, convertDicts: dict, pre_comp_counts: bool) -> None: # Process Kaggle Display Advertising Challenge or Terabyte Dataset # by converting unicode strings in X_cat to integers and # converting negative integer values in X_int. @@ -89,28 +91,23 @@ def processCriteoAdData(d_path, d_file, npzfile, i, convertDicts, pre_comp_count y=y, ) logging.debug("Processed " + filename_i, end="\n") - # sanity check (applicable only if counts have been pre-computed & are re-computed) - # for j in range(26): - # if pre_comp_counts[j] != counts[j]: - # sys.exit("ERROR: Sanity check on counts has failed") - # logging.debug("\nSanity check on counts passed") return def concatCriteoAdData( - d_path, - d_file, - npzfile, - trafile, - days, - data_split, - randomize, - total_per_file, - total_count, - memory_map, - o_filename -): + d_path: Any, + d_file: Any, + npzfile: Any, + trafile: Any, + days: Any, + data_split: Any, + randomize: Any, + total_per_file: Any, + total_count: Any, + memory_map: Any, + o_filename: Any +) -> str: # Concatenates different days and saves the result. # # Inputs: @@ -316,17 +313,17 @@ def concatCriteoAdData( def getCriteoAdData( - datafile, - o_filename, - max_ind_range=-1, - sub_sample_rate=0.0, - days=7, - data_split='train', - randomize='total', - criteo_kaggle=True, - memory_map=False, - dataset_multiprocessing=False, -): + datafile: str, + o_filename: str, + max_ind_range: int = -1, + sub_sample_rate: int = 0.0, + days: int = 7, + data_split: str = 'train', + randomize: str = 'total', + criteo_kaggle: bool = True, + memory_map: bool = False, + dataset_multiprocessing: bool = False, +) -> str: # Passes through entire dataset and defines dictionaries for categorical # features and determines the number of total categories. # @@ -407,14 +404,14 @@ def getCriteoAdData( # process a file worth of data and reinitialize data # note that a file main contain a single or multiple splits def process_one_file( - datfile, - npzfile, - split, - num_data_in_split, - dataset_multiprocessing, - convertDictsDay=None, - resultDay=None - ): + datfile: Any, + npzfile: Any, + split: Any, + num_data_in_split: Any, + dataset_multiprocessing: Any, + convertDictsDay: Any = None, + resultDay: Any = None + ) -> Union[None, int]: if dataset_multiprocessing: convertDicts_day = [{} for _ in range(26)] diff --git a/examples/dlrm/facebook_dataloading/dataloading_fb.py b/examples/dlrm/facebook_dataloading/dataloading_fb.py index 5618c44..bd37a30 100644 --- a/examples/dlrm/facebook_dataloading/dataloading_fb.py +++ b/examples/dlrm/facebook_dataloading/dataloading_fb.py @@ -1,3 +1,4 @@ +# type: ignore # Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the @@ -22,6 +23,7 @@ from os import path import sys import logging +from typing import Any, List, Tuple from . import data_utils @@ -44,17 +46,17 @@ class CriteoDataset(Dataset): def __init__( self, - dataset, - max_ind_range, - sub_sample_rate, - randomize, - split="train", - raw_path="", - pro_data="", - memory_map=False, - dataset_multiprocessing=False, - store_all_indices=False, - ): + dataset: str, + max_ind_range: int, + sub_sample_rate: int, + randomize: bool, + split: str = "train", + raw_path: str = "", + pro_data: str = "", + memory_map: str = False, + dataset_multiprocessing: str = False, + store_all_indices: str = False, + ) -> None: # dataset # tar_fea = 1 # single target den_fea = 13 # 13 dense features @@ -139,41 +141,6 @@ def __init__( else: sys.exit("ERROR: dataset split is neither none, nor train or test.") - ''' - # text - logging.debug("text") - for i in range(days): - fi = self.npzfile + "_{0}".format(i) - with open(fi) as data: - ttt = 0; nnn = 0 - for _j, line in enumerate(data): - ttt +=1 - if np.int32(line[0]) > 0: - nnn +=1 - logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" - + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") - # processed - logging.debug("processed") - for i in range(days): - fi = self.npzfile + "_{0}_processed.npz".format(i) - with np.load(fi) as data: - yyy = data["y"] - ttt = len(yyy) - nnn = np.count_nonzero(yyy) - logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" - + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") - # reordered - logging.debug("reordered") - for i in range(days): - fi = self.npzfile + "_{0}_reordered.npz".format(i) - with np.load(fi) as data: - yyy = data["y"] - ttt = len(yyy) - nnn = np.count_nonzero(yyy) - logging.debug("day=" + str(i) + " total=" + str(ttt) + " non-zeros=" - + str(nnn) + " ratio=" +str((nnn * 100.) / ttt) + "%") - ''' - # load unique counts with np.load(self.d_path + self.d_file + "_fea_count.npz") as data: self.counts = data["counts"] @@ -278,7 +245,7 @@ def __init__( logging.debug("Split data according to indices...") - def __getitem__(self, index): + def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: if isinstance(index, slice): return [ @@ -317,7 +284,7 @@ def __getitem__(self, index): else: return self.X_int[i], self.X_cat[i], self.y[i] - def _default_preprocess(self, X_int, X_cat, y): + def _default_preprocess(self, X_int: torch.Tensor, X_cat: torch.Tensor, y: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: X_int = torch.log(torch.tensor(X_int, dtype=torch.float) + 1) if self.max_ind_range > 0: X_cat = torch.tensor(X_cat % self.max_ind_range, dtype=torch.long) @@ -327,7 +294,7 @@ def _default_preprocess(self, X_int, X_cat, y): return X_int, X_cat, y - def __len__(self): + def __len__(self) -> int: if self.memory_map: if self.split == 'none': return self.offset_per_file[-1] @@ -343,7 +310,7 @@ def __len__(self): return len(self.y) -def collate_wrapper_criteo_offset(list_of_tuples): +def collate_wrapper_criteo_offset(list_of_tuples: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: # where each tuple is (X_int, X_cat, y) # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) # transposed_data = list(zip(*list_of_tuples)) @@ -363,8 +330,8 @@ def collate_wrapper_criteo_offset(list_of_tuples): # Conversion from offset to length -def offset_to_length_converter(lS_o, lS_i): - def diff(tensor): +def offset_to_length_converter(lS_o: torch.Tensor, lS_i: torch.Tensor) -> torch.Tensor: + def diff(tensor: torch.Tensor) -> torch.Tensor: return tensor[1:] - tensor[:-1] return torch.stack( @@ -375,10 +342,10 @@ def diff(tensor): ) -def collate_wrapper_criteo_length(list_of_tuples): +def collate_wrapper_criteo_length(list_of_tuples: List[Any]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: # where each tuple is (X_int, X_cat, y) # transposed_data = list(zip(*list_of_tuples)) - + transposed_data = list(np.array(i) for i in zip(*list_of_tuples)) # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) X_int = torch.log(torch.tensor(transposed_data[0], dtype=torch.float) + 1) diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index 11b7a51..4cfc6b0 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -13,7 +13,7 @@ import argparse import logging from pathlib import Path -from typing import List, Any +from typing import List, Any, Tuple import fvbitcore.nn as fv_nn import wandb @@ -39,21 +39,23 @@ logger = logging.getLogger() -def make_dlrm_dataloaders(dataset_dir, download, ignore_size, batch_size, batch_size_test, num_workers): - # train_dataset, test_dataset = Criteo.get_train_and_test( # type: ignore - # root_directory=dataset_dir, download=download, augmentation=None - # ) +def make_dlrm_dataloaders(dataset_dir: Path, download: bool, ignore_size: float, batch_size: int, batch_size_test: int, num_workers: int) -> Tuple[DataLoader, DataLoader, List[int], int]: + """Creates test and train dataloaders for dlrm + + Args: + dataset_dir (Path): path to dataset (to be stored or existent) + download (bool): weather dataset should be downloaded + ignore_size (dloat): portion of dataset to ignore while training + batch_size (int): batch size + batch_size_test (int): batch size to be used in test loader (might be larger) + num_workers (int): number of workers to be used in dataloader + + Returns: + Tuple[Dataloader, Dataloader, int, int]: the dataloaders, the size of the dense features and the size of embedding layers + """ logging.info("loading Criteo dataset...") dataset = Criteo(True, root_directory=dataset_dir, download=download, augmentation=None).dataset - # train_indices = np.arange(len(train_dataset)) - # np.random.shuffle(train_indices) - # train_subset_random_sampler = SubsetRandomSampler(train_indices[:int((1 - ignore_size) * len(train_dataset))]) - - # val_indices = np.arange(len(test_dataset)) - # np.random.shuffle(val_indices) - # val_subset_random_sampler = SubsetRandomSampler(val_indices[:int((1 - ignore_size) * len(test_dataset))]) - train_dataset = SplitCriteoDataset(dataset, "train", ignore_size=ignore_size) test_dataset = SplitCriteoDataset(dataset, "test", ignore_size=ignore_size) logging.info(f"loaded {len(train_dataset)} train and {len(test_dataset)} test samples!") @@ -165,39 +167,12 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: callbacks=callbacks, # type: ignore log_every_n_steps=args.log_interval, ) - # augmentation_level = Augmentation.from_string(args.augmentation) logger.info(f"model: {args.model}") logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") logger.info(f"max_epochs: {args.max_epochs}") - # if args.fake_data: - # logger.info(f"dummy dataset: {dataset.name} (not using real data!)") - # train_dataset, test_dataset = dataset.get_dummy_train_and_test_datasets() # type: ignore - # else: - # logger.info(f"dataset: {dataset.name}") - # train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore - # root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level - # ) - # train_loader = DataLoader( - # train_dataset, - # batch_size=args.batch_size, - # num_workers=args.num_workers, - # shuffle=True, - # pin_memory=True, - # persistent_workers=True, - # ) # type: ignore - # test_loader = DataLoader( - # test_dataset, - # batch_size=args.batch_size, - # num_workers=args.num_workers, - # shuffle=False, - # pin_memory=True, - # persistent_workers=True, - # ) # type: ignore - data_point = iter(train_loader).next() data_point = (data_point[0], (data_point[1], data_point[2])) - # data_point = torch.zeros(iter(train_loader).next().shape) computational_intensity = fv_nn.FlopCountAnalysis(model, inputs=data_point, quantization_base_class=Quantization) stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) diff --git a/examples/dlrm/utils/lightning_model.py b/examples/dlrm/utils/lightning_model.py index 968f9c6..b14ee81 100644 --- a/examples/dlrm/utils/lightning_model.py +++ b/examples/dlrm/utils/lightning_model.py @@ -2,7 +2,7 @@ from sklearn import metrics import logging from argparse import Namespace -from typing import Union +from typing import Union, Any, List import torch import torch.nn.functional as F @@ -15,6 +15,8 @@ class ModelWrapper(LightningModule): + """Wrapper class for a pytorch model to fully utilize pytorch lightning functionality + """ def __init__( self, model: Module, @@ -51,7 +53,7 @@ def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: i else: return optimizer - def training_step(self, batch, batch_idx): + def training_step(self, batch: torch.Tensor, batch_idx: int) -> torch.Tensor: # type: ignore dense_values, categorical_values_i, categorical_values_o, y = batch if isinstance(categorical_values_i, list): for el in categorical_values_i: @@ -70,7 +72,7 @@ def training_step(self, batch, batch_idx): self.log_dict({"loss/train": loss}) return loss - def validation_step_end(self, *args, **kwargs): + def validation_step_end(self, *args: Any, **kwargs: Any) -> Any: """calculate all them metrics and log via wandb/tensorboard""" y = torch.cat(list(map(lambda x: x["y"], self.validation_results))) @@ -98,10 +100,10 @@ def validation_step_end(self, *args, **kwargs): return super().validation_step_end(*args, **kwargs) def on_validation_start(self) -> None: - self.validation_results = [] + self.validation_results: List[dict] = [] return super().on_validation_start() - def validation_step(self, batch, batch_idx): + def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: ignore dense_values, categorical_values_i, categorical_values_o, y = batch dense_values = dense_values.to(self.device) if isinstance(categorical_values_i, list): From 3f0db4ac686320e928198dcf016c902495f8655c Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 29 Jul 2022 09:07:04 +0200 Subject: [PATCH 104/208] added docstrings --- bitorch/models/dlrm.py | 16 +++- .../dlrm/facebook_dataloading/__init__.py | 1 + .../dlrm/facebook_dataloading/data_utils.py | 80 ++++++++++++------- .../facebook_dataloading/dataloading_fb.py | 38 +++++++-- examples/dlrm/utils/lightning_model.py | 5 +- 5 files changed, 99 insertions(+), 41 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index ab2782b..1917ca1 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -23,6 +23,15 @@ def parse_layer_sizes(layer_sizes_str: Union[List[int], str]) -> List[int]: + """parses layer sizes passed as string via cli arg + + Args: + layer_sizes_str (Union[List[int], str]): either list of layer sizes in which case the input is just returned or + a string in format '[layer size a, layer size b, etc]' + + Returns: + List[int]: list of layer sizes + """ if isinstance(layer_sizes_str, list): return [int(size) for size in layer_sizes_str] layer_sizes_str = layer_sizes_str.replace('[', '').replace(']', '') @@ -83,7 +92,8 @@ def create_embeddings( embedding_layers = ModuleList() for layer_size in layer_sizes: logging.info( - f"creating embedding layer with {layer_size} * {embedding_dimension} = {layer_size * embedding_dimension} params...") + f"creating embedding layer with {layer_size} * {embedding_dimension} = " + f"{layer_size * embedding_dimension} params...") if quantized: embedding_layers.append(QEmbeddingBag( layer_size, @@ -139,7 +149,9 @@ def __init__( top_mlp_layer_sizes = [(len(embedding_layer_sizes) + 1) * embedding_dimension, *top_mlp_layer_sizes] elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: top_mlp_layer_sizes = [ - embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), *top_mlp_layer_sizes] + embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), + *top_mlp_layer_sizes + ] self.bottom_mlp = create_mlp( bottom_mlp_layer_sizes, quantized=binary_bottom_mlp, diff --git a/examples/dlrm/facebook_dataloading/__init__.py b/examples/dlrm/facebook_dataloading/__init__.py index e69de29..73448bd 100644 --- a/examples/dlrm/facebook_dataloading/__init__.py +++ b/examples/dlrm/facebook_dataloading/__init__.py @@ -0,0 +1 @@ +"""Criteo Dataloading for DLRM partially copied from here: https://github.com/facebookresearch/dlrm""" diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py index 93c177c..a21fba7 100644 --- a/examples/dlrm/facebook_dataloading/data_utils.py +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -52,17 +52,27 @@ import numpy as np -def processCriteoAdData(d_path: str, d_file: str, npzfile: str, i: int, convertDicts: dict, pre_comp_counts: bool) -> None: - # Process Kaggle Display Advertising Challenge or Terabyte Dataset - # by converting unicode strings in X_cat to integers and - # converting negative integer values in X_int. - # - # Loads data in the form "{kaggle|terabyte}_day_i.npz" where i is the day. - # - # Inputs: - # d_path (str): path for {kaggle|terabyte}_day_i.npz files - # i (int): splits in the dataset (typically 0 to 7 or 0 to 24) - +def processCriteoAdData( + d_path: str, + d_file: str, + npzfile: str, + i: int, + convertDicts: dict, + pre_comp_counts: bool) -> None: + """Process Kaggle Display Advertising Challenge or Terabyte Dataset + by converting unicode strings in X_cat to integers and + converting negative integer values in X_int. + + Loads data in the form "{kaggle|terabyte}_day_i.npz" where i is the day. + + Args: + d_path (str): path for {kaggle|terabyte}_day_i.npz files + d_file (str): _description_ + npzfile (str): _description_ + i (int): splits in the dataset (typically 0 to 7 or 0 to 24) + convertDicts (dict): _description_ + pre_comp_counts (bool): _description_ + """ # process data if not all files exist filename_i = npzfile + "_{0}_processed.npz".format(i) @@ -108,15 +118,16 @@ def concatCriteoAdData( memory_map: Any, o_filename: Any ) -> str: - # Concatenates different days and saves the result. - # - # Inputs: - # days (int): total number of days in the dataset (typically 7 or 24) - # d_path (str): path for {kaggle|terabyte}_day_i.npz files - # o_filename (str): output file name - # - # Output: - # o_file (str): output file path + """Concatenates different days and saves the result. + + Args: + days (int): total number of days in the dataset (typically 7 or 24) + d_path (str): path for {kaggle|terabyte}_day_i.npz files + o_filename (str): output file name + + Return: + o_file (str): output file path + """ if memory_map: # dataset break up per fea @@ -324,15 +335,16 @@ def getCriteoAdData( memory_map: bool = False, dataset_multiprocessing: bool = False, ) -> str: - # Passes through entire dataset and defines dictionaries for categorical - # features and determines the number of total categories. - # - # Inputs: - # datafile : path to downloaded raw data file - # o_filename (str): saves results under o_filename if filename is not "" - # - # Output: - # o_file (str): output file path + """Passes through entire dataset and defines dictionaries for categorical + features and determines the number of total categories. + + Inputs: + datafile : path to downloaded raw data file + o_filename (str): saves results under o_filename if filename is not "" + + Output: + o_file (str): output file path + """ # split the datafile into path and filename lstr = datafile.split("/") @@ -381,7 +393,10 @@ def getCriteoAdData( nf.write(line) nf.close() else: - sys.exit("ERROR: Criteo Kaggle Display Ad Challenge Dataset path is invalid; please download from https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset") + sys.exit( + "ERROR: Criteo Kaggle Display Ad Challenge Dataset path is invalid; please download from " + "https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset" + ) else: # WARNING: The raw data consist of day_0.gz,... ,day_23.gz text files # Each line in the file is a sample, consisting of 13 continuous and @@ -399,7 +414,10 @@ def getCriteoAdData( total_per_file.append(total_per_file_count) total_count += total_per_file_count else: - sys.exit("ERROR: Criteo Terabyte Dataset path is invalid; please download from https://labs.criteo.com/2013/12/download-terabyte-click-logs") + sys.exit( + "ERROR: Criteo Terabyte Dataset path is invalid; please download " + "from https://labs.criteo.com/2013/12/download-terabyte-click-logs" + ) # process a file worth of data and reinitialize data # note that a file main contain a single or multiple splits diff --git a/examples/dlrm/facebook_dataloading/dataloading_fb.py b/examples/dlrm/facebook_dataloading/dataloading_fb.py index bd37a30..b42dcd7 100644 --- a/examples/dlrm/facebook_dataloading/dataloading_fb.py +++ b/examples/dlrm/facebook_dataloading/dataloading_fb.py @@ -284,7 +284,11 @@ def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Ten else: return self.X_int[i], self.X_cat[i], self.y[i] - def _default_preprocess(self, X_int: torch.Tensor, X_cat: torch.Tensor, y: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + def _default_preprocess( + self, + X_int: torch.Tensor, + X_cat: torch.Tensor, + y: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: X_int = torch.log(torch.tensor(X_int, dtype=torch.float) + 1) if self.max_ind_range > 0: X_cat = torch.tensor(X_cat % self.max_ind_range, dtype=torch.long) @@ -310,7 +314,16 @@ def __len__(self) -> int: return len(self.y) -def collate_wrapper_criteo_offset(list_of_tuples: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: +def collate_wrapper_criteo_offset( + list_of_tuples: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """collates the input features into processabel tensors + + Args: + list_of_tuples (List[torch.Tensor]): input tensors + + Returns: + Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: output + """ # where each tuple is (X_int, X_cat, y) # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) # transposed_data = list(zip(*list_of_tuples)) @@ -331,6 +344,15 @@ def collate_wrapper_criteo_offset(list_of_tuples: List[torch.Tensor]) -> Tuple[t # Conversion from offset to length def offset_to_length_converter(lS_o: torch.Tensor, lS_i: torch.Tensor) -> torch.Tensor: + """converts the offsets of categorical features into tensors containing length + + Args: + lS_o (torch.Tensor): offset tensors + lS_i (torch.Tensor): indices + + Returns: + torch.Tensor: lengths + """ def diff(tensor: torch.Tensor) -> torch.Tensor: return tensor[1:] - tensor[:-1] @@ -342,10 +364,16 @@ def diff(tensor: torch.Tensor) -> torch.Tensor: ) -def collate_wrapper_criteo_length(list_of_tuples: List[Any]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: - # where each tuple is (X_int, X_cat, y) - # transposed_data = list(zip(*list_of_tuples)) +def collate_wrapper_criteo_length( + list_of_tuples: List[Any]) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """collates the input features into processabel tensors + + Args: + list_of_tuples (List[torch.Tensor]): input tensors + Returns: + Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: output + """ transposed_data = list(np.array(i) for i in zip(*list_of_tuples)) # transposed_data = np.array(list(zip(*list_of_tuples)), dtype=np.float32) X_int = torch.log(torch.tensor(transposed_data[0], dtype=torch.float) + 1) diff --git a/examples/dlrm/utils/lightning_model.py b/examples/dlrm/utils/lightning_model.py index b14ee81..2409f68 100644 --- a/examples/dlrm/utils/lightning_model.py +++ b/examples/dlrm/utils/lightning_model.py @@ -15,8 +15,8 @@ class ModelWrapper(LightningModule): - """Wrapper class for a pytorch model to fully utilize pytorch lightning functionality - """ + """Wrapper class for a pytorch model to fully utilize pytorch lightning functionality""" + def __init__( self, model: Module, @@ -74,7 +74,6 @@ def training_step(self, batch: torch.Tensor, batch_idx: int) -> torch.Tensor: # def validation_step_end(self, *args: Any, **kwargs: Any) -> Any: """calculate all them metrics and log via wandb/tensorboard""" - y = torch.cat(list(map(lambda x: x["y"], self.validation_results))) y_hat = torch.cat(list(map(lambda x: x["y_hat"], self.validation_results))) loss = self.loss_function(y, y_hat) From 47e6e9fc3e19d38e52916e8a48ff7b17e6956d2c Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 29 Jul 2022 10:30:04 +0200 Subject: [PATCH 105/208] removed blank lines --- examples/dlrm/facebook_dataloading/data_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py index a21fba7..c37d4a1 100644 --- a/examples/dlrm/facebook_dataloading/data_utils.py +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -128,7 +128,6 @@ def concatCriteoAdData( Return: o_file (str): output file path """ - if memory_map: # dataset break up per fea # tar_fea = 1 # single target @@ -345,7 +344,6 @@ def getCriteoAdData( Output: o_file (str): output file path """ - # split the datafile into path and filename lstr = datafile.split("/") d_path = "/".join(lstr[0:-1]) + "/" From b57a9969178aeab225ab05da0aadb3c45f7d4661 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 29 Jul 2022 10:42:01 +0200 Subject: [PATCH 106/208] applied black --- bitorch/models/dlrm.py | 110 +++++++++--------- examples/dlrm/criteo.py | 9 +- .../dlrm/facebook_dataloading/data_utils.py | 2 +- .../facebook_dataloading/dataloading_fb.py | 2 +- examples/dlrm/train_dlrm.py | 4 +- examples/dlrm/utils/lightning_model.py | 23 ++-- mypy.ini | 2 +- 7 files changed, 77 insertions(+), 75 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 1917ca1..c12f426 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -3,16 +3,7 @@ from typing import Any, List, Union import logging import torch -from torch.nn import ( - Linear, - Sequential, - PReLU, - Sigmoid, - EmbeddingBag, - ModuleList, - BatchNorm1d, - Module -) +from torch.nn import Linear, Sequential, PReLU, Sigmoid, EmbeddingBag, ModuleList, BatchNorm1d, Module import numpy as np from bitorch.datasets.base import BasicDataset from bitorch.layers import QLinear @@ -34,7 +25,7 @@ def parse_layer_sizes(layer_sizes_str: Union[List[int], str]) -> List[int]: """ if isinstance(layer_sizes_str, list): return [int(size) for size in layer_sizes_str] - layer_sizes_str = layer_sizes_str.replace('[', '').replace(']', '') + layer_sizes_str = layer_sizes_str.replace("[", "").replace("]", "") return [int(size) for size in layer_sizes_str.split(",")] @@ -44,9 +35,7 @@ class Interaction_Operation_Type(Enum): SUM = "sum" -def create_mlp( - layer_sizes: List[int], - quantized: bool = False) -> Sequential: +def create_mlp(layer_sizes: List[int], quantized: bool = False) -> Sequential: """creates a mlp module Args: @@ -62,8 +51,7 @@ def create_mlp( output_size = layer_size mlp_layers.append(BatchNorm1d(input_size)) mlp_layers.append( - QLinear(input_size, output_size, bias=False) if quantized else - Linear(input_size, output_size, bias=True) + QLinear(input_size, output_size, bias=False) if quantized else Linear(input_size, output_size, bias=True) ) mean = 0.0 # std_dev = np.sqrt(variance) std_dev = np.sqrt(2 / (output_size + input_size)) # np.sqrt(1 / m) # np.sqrt(1 / n) @@ -82,10 +70,8 @@ def create_mlp( def create_embeddings( - embedding_dimension: int, - layer_sizes: List[int], - quantized: bool, - sparse: bool = False) -> ModuleList: + embedding_dimension: int, layer_sizes: List[int], quantized: bool, sparse: bool = False +) -> ModuleList: """creates the embedding layers for each category.""" if sparse: logging.info("USING SPARSE EMBEDDINGS") @@ -93,14 +79,17 @@ def create_embeddings( for layer_size in layer_sizes: logging.info( f"creating embedding layer with {layer_size} * {embedding_dimension} = " - f"{layer_size * embedding_dimension} params...") + f"{layer_size * embedding_dimension} params..." + ) if quantized: - embedding_layers.append(QEmbeddingBag( - layer_size, - embedding_dim=embedding_dimension, - mode="mean", - sparse=sparse, - )) + embedding_layers.append( + QEmbeddingBag( + layer_size, + embedding_dim=embedding_dimension, + mode="mean", + sparse=sparse, + ) + ) else: embedding_layers.append(EmbeddingBag(layer_size, embedding_dimension, mode="sum", sparse=sparse)) embedding_weights = np.random.uniform( @@ -118,18 +107,19 @@ class DLRM(Model): validation_results: List[dict] = [] def __init__( - self, - dataset: BasicDataset, - dense_feature_size: int, - embedding_dimension: int, - embedding_layer_sizes: List[int], - bottom_mlp_layer_sizes: Union[List[int], str], - top_mlp_layer_sizes: Union[List[int], str], - interaction_operation: Interaction_Operation_Type, - binary_bottom_mlp: bool, - binary_top_mlp: bool, - binary_embedding: bool, - **kwargs: Any) -> None: + self, + dataset: BasicDataset, + dense_feature_size: int, + embedding_dimension: int, + embedding_layer_sizes: List[int], + bottom_mlp_layer_sizes: Union[List[int], str], + top_mlp_layer_sizes: Union[List[int], str], + interaction_operation: Interaction_Operation_Type, + binary_bottom_mlp: bool, + binary_top_mlp: bool, + binary_embedding: bool, + **kwargs: Any, + ) -> None: super().__init__(dataset) self.interaction_operation = interaction_operation self.embedding_layers = create_embeddings( @@ -150,7 +140,7 @@ def __init__( elif interaction_operation == Interaction_Operation_Type.PRODUCT.value: top_mlp_layer_sizes = [ embedding_dimension + (len(embedding_layer_sizes) + 1) * ((len(embedding_layer_sizes) + 1) // 2), - *top_mlp_layer_sizes + *top_mlp_layer_sizes, ] self.bottom_mlp = create_mlp( bottom_mlp_layer_sizes, @@ -165,29 +155,33 @@ def __init__( @staticmethod def add_argparse_arguments(parent_parser: ArgumentParser) -> None: parser = parent_parser.add_argument_group("DLRM Model") - parser.add_argument("--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64]", - help="layer sizes of the bottom mlp") - parser.add_argument("--top-mlp-layer-sizes", type=str, default="[512, 256, 1]", - help="layer sizes of the top mlp") - parser.add_argument("--embedding-dimension", type=int, default=16, - help="number of embedding dimensions") + parser.add_argument( + "--bottom-mlp-layer-sizes", type=str, default="[512, 256, 64]", help="layer sizes of the bottom mlp" + ) + parser.add_argument( + "--top-mlp-layer-sizes", type=str, default="[512, 256, 1]", help="layer sizes of the top mlp" + ) + parser.add_argument("--embedding-dimension", type=int, default=16, help="number of embedding dimensions") parser.add_argument( "--interaction-operation", - choices=[ - Interaction_Operation_Type.CONCAT.value, - Interaction_Operation_Type.PRODUCT.value], - default=Interaction_Operation_Type.PRODUCT.value) + choices=[Interaction_Operation_Type.CONCAT.value, Interaction_Operation_Type.PRODUCT.value], + default=Interaction_Operation_Type.PRODUCT.value, + ) parser.add_argument("--dense-embeddings", action="store_false", help="Disable sparse embeddings") - parser.add_argument("--binary-embedding", action="store_true", default=True, - help="toggles use of binary embeddings in model.") - parser.add_argument("--binary-top-mlp", action="store_true", default=True, - help="toggles use of binary top mlp in model.") - parser.add_argument("--binary-bottom-mlp", action="store_true", default=False, - help="toggles use of binary bottom mlp in model.") + parser.add_argument( + "--binary-embedding", action="store_true", default=True, help="toggles use of binary embeddings in model." + ) + parser.add_argument( + "--binary-top-mlp", action="store_true", default=True, help="toggles use of binary top mlp in model." + ) + parser.add_argument( + "--binary-bottom-mlp", action="store_true", default=False, help="toggles use of binary bottom mlp in model." + ) - def forward_embeddings(self, categorical_values_i: torch.Tensor, - categorical_values_o: torch.Tensor) -> List[torch.Tensor]: + def forward_embeddings( + self, categorical_values_i: torch.Tensor, categorical_values_o: torch.Tensor + ) -> List[torch.Tensor]: """forwards the preprocessed data through the embedding layers.""" embedding_outputs = [] for index, embedding_layer in enumerate(self.embedding_layers): diff --git a/examples/dlrm/criteo.py b/examples/dlrm/criteo.py index fb7e498..caeef61 100644 --- a/examples/dlrm/criteo.py +++ b/examples/dlrm/criteo.py @@ -11,7 +11,10 @@ class SplitCriteoDataset(Dataset): """Dataset to get items from a dataset for each split. Useful if dataset creation takes a lot of time and can be done exactly once.""" - def __init__(self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0) -> None: + + def __init__( + self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0 + ) -> None: self.dataset = dataset self.indices = self.dataset.train_indices if split == "train" else self.dataset.test_indices @@ -30,7 +33,7 @@ class Criteo(BasicDataset): num_train_samples = 60000 num_val_samples = 10000 - dataset_url = 'http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz' + dataset_url = "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz" def get_dataset(self, download: bool = True) -> Dataset: try: @@ -43,7 +46,7 @@ def get_dataset(self, download: bool = True) -> Dataset: if result != 0: raise Exception("Download failed") logging.info("FINISHED DOWNLOAD") - if not (self.root_directory / 'train.txt').exists(): + if not (self.root_directory / "train.txt").exists(): logging.info("EXTRACTING CRITEO DATASET") result = os.system(f"tar -xf {str(self.root_directory / 'criteo.tar.gz')} -C {self.root_directory}") if result != 0: diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py index c37d4a1..61e195e 100644 --- a/examples/dlrm/facebook_dataloading/data_utils.py +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -1,4 +1,4 @@ -# type: ignore +# fmt: off # Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the diff --git a/examples/dlrm/facebook_dataloading/dataloading_fb.py b/examples/dlrm/facebook_dataloading/dataloading_fb.py index b42dcd7..a3a2e6e 100644 --- a/examples/dlrm/facebook_dataloading/dataloading_fb.py +++ b/examples/dlrm/facebook_dataloading/dataloading_fb.py @@ -1,4 +1,4 @@ -# type: ignore +# fmt: off # Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index 4cfc6b0..969e518 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -39,7 +39,9 @@ logger = logging.getLogger() -def make_dlrm_dataloaders(dataset_dir: Path, download: bool, ignore_size: float, batch_size: int, batch_size_test: int, num_workers: int) -> Tuple[DataLoader, DataLoader, List[int], int]: +def make_dlrm_dataloaders( + dataset_dir: Path, download: bool, ignore_size: float, batch_size: int, batch_size_test: int, num_workers: int +) -> Tuple[DataLoader, DataLoader, List[int], int]: """Creates test and train dataloaders for dlrm Args: diff --git a/examples/dlrm/utils/lightning_model.py b/examples/dlrm/utils/lightning_model.py index 2409f68..8c858f7 100644 --- a/examples/dlrm/utils/lightning_model.py +++ b/examples/dlrm/utils/lightning_model.py @@ -86,16 +86,19 @@ def validation_step_end(self, *args: Any, **kwargs: Any) -> Any: roc_auc = metrics.roc_auc_score(y_array, y_hat.cpu()) precision = metrics.precision_score(y_array, y_hat_array) recall = metrics.recall_score(y_array, y_hat_array) - self.log_dict({ - "val_los": loss, - "val_rmse": rmse, - "roc_auc": roc_auc, - "precision": precision, - "recall": recall, - "balanced accuracy": balanced_accuracy, - "accuracy": accuracy, - "f1 score": f1, - }, prog_bar=True) + self.log_dict( + { + "val_los": loss, + "val_rmse": rmse, + "roc_auc": roc_auc, + "precision": precision, + "recall": recall, + "balanced accuracy": balanced_accuracy, + "accuracy": accuracy, + "f1 score": f1, + }, + prog_bar=True, + ) return super().validation_step_end(*args, **kwargs) def on_validation_start(self) -> None: diff --git a/mypy.ini b/mypy.ini index 9084fb0..177e95e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,7 +8,7 @@ ignore_missing_imports = True disallow_untyped_defs = True disallow_any_explicit = False disable_error_code = attr-defined -exclude = examples/mnist +exclude = examples/(mnist|dlrm) [mypy-torchvision.io._video_opt.*] From d4f2a72059241b760076d15463364a2ef8ea70e8 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 29 Jul 2022 13:58:40 +0200 Subject: [PATCH 107/208] edited changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c41da..6ab6523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - support for integration of bitorch's inference engine for the following layers - QLinear - QConv +- a quantized DLRM version, derived from [this](https://github.com/facebookresearch/dlrm) implementation +- example code for training the quantized DLRM model ### Changed From af7eece74e01d106f92afaed24fa987de8f5dd24 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 11 Aug 2022 15:33:35 +0200 Subject: [PATCH 108/208] add check for compatibility with older versions --- .gitlab-ci.yml | 9 +++++++++ requirements.txt | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8025438..fd84e26 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,6 +57,15 @@ test:3.10: - .scheduled-only image: python:3.10 +test:torch-compatibility: + extends: + - .test + - .scheduled-only + script: + - pip install torch==1.9.0 torchvision==0.10.0 --extra-index-url https://download.pytorch.org/whl/cu113 + - pytest --version + - python -m pytest . + # Documentation test-build-doc: diff --git a/requirements.txt b/requirements.txt index f34e435..6988b36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -torch~=1.12.0 -torchvision~=0.13.0 +torch>=1.9.0 +torchvision>=0.10.0 matplotlib numpy From 187dd952047f482d13ec5044ce4c727e7fdc8efc Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 11 Aug 2022 15:34:43 +0200 Subject: [PATCH 109/208] run compatibility test every pipeline --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fd84e26..a47aef5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,7 +60,6 @@ test:3.10: test:torch-compatibility: extends: - .test - - .scheduled-only script: - pip install torch==1.9.0 torchvision==0.10.0 --extra-index-url https://download.pytorch.org/whl/cu113 - pytest --version From e85a2248ad5b5cb7a9a2a2d568cd4b55fd8ae72a Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 1 Aug 2022 08:51:09 +0200 Subject: [PATCH 110/208] change model names --- bitorch/models/__init__.py | 6 +++--- bitorch/models/base.py | 2 +- bitorch/models/densenet.py | 10 +++++----- bitorch/models/lenet.py | 2 +- bitorch/models/meliusnet.py | 26 +++++++++++++------------- bitorch/models/resnet.py | 18 +++++++++--------- bitorch/models/resnet_e.py | 6 +++--- bitorch/util.py | 4 ++-- tests/models/test_model_names.py | 13 +++++++++++++ 9 files changed, 50 insertions(+), 37 deletions(-) create mode 100644 tests/models/test_model_names.py diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index 160b815..1ce4fa7 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -77,7 +77,7 @@ ] -models_by_name = build_lookup_dictionary(__name__, __all__, Model) +models_by_name = build_lookup_dictionary(__name__, __all__, Model, key_fn=lambda x: x.name.lower()) def model_from_name(name: str) -> Type[Model]: @@ -93,9 +93,9 @@ def model_from_name(name: str) -> Type[Model]: Returns: Model: the model """ - if name not in models_by_name: + if name.lower() not in models_by_name: raise ValueError(f"{name} model not found!") - return models_by_name[name] + return models_by_name[name.lower()] def model_names() -> List: diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 8da4e16..ac4b3ec 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -15,7 +15,7 @@ class Model(nn.Module): """Base class for Bitorch models""" - name = "None" + name = "" def __init__(self, dataset: Union[BasicDataset, Type[BasicDataset]]) -> None: super(Model, self).__init__() diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 5170c33..7e92cfd 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -216,7 +216,7 @@ def basedensenet_constructor( class DenseNet(Model): - name = "densenet" + name = "DenseNet" densenet_spec = { # block_config, reduction_factor, downsampling None: (None, [1 / 2, 1 / 2, 1 / 2], DOWNSAMPLE_STRUCT), @@ -295,7 +295,7 @@ class DenseNetFlex(DenseNet): """ - name = "densenetflex" + name = "DenseNetFlex" def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNetFlex, self).__init__(None, *args, **kwargs) @@ -318,7 +318,7 @@ class DenseNet28(DenseNet): https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ - name = "densenet28" + name = "DenseNet28" def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet28, self).__init__(28, *args, **kwargs) @@ -336,7 +336,7 @@ class DenseNet37(DenseNet): https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ - name = "densenet37" + name = "DenseNet37" def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet37, self).__init__(37, *args, **kwargs) @@ -354,7 +354,7 @@ class DenseNet45(DenseNet): https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html """ - name = "densenet45" + name = "DenseNet45" def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet45, self).__init__(45, *args, **kwargs) diff --git a/bitorch/models/lenet.py b/bitorch/models/lenet.py index 7dd2047..f7f915d 100644 --- a/bitorch/models/lenet.py +++ b/bitorch/models/lenet.py @@ -12,7 +12,7 @@ class LeNet(Model): num_channels_conv = 64 activation_function = nn.Tanh num_fc = 1000 - name = "lenet" + name = "LeNet" def generate_quant_model( self, diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 9c71738..5f7a0d2 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -56,7 +56,7 @@ def _add_base_block_structure(self, layer_num: int, dilation: int) -> None: class MeliusNet(Model): - name = "meliusnet" + name = "MeliusNet" meliusnet_spec = { # name: block_config, reduction_factors, downsampling @@ -139,7 +139,7 @@ class MeliusNetFlex(MeliusNet): ` paper. """ - name = "meliusnetflex" + name = "MeliusNetFlex" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetFlex, self).__init__(None, *args, **kwargs) @@ -160,7 +160,7 @@ class MeliusNet22(MeliusNet): ` paper. """ - name = "meliusnet22" + name = "MeliusNet22" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet22, self).__init__("22", *args, **kwargs) @@ -175,7 +175,7 @@ class MeliusNet23(MeliusNet): ` paper. """ - name = "meliusnet23" + name = "MeliusNet23" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet23, self).__init__("23", *args, **kwargs) @@ -190,7 +190,7 @@ class MeliusNet29(MeliusNet): ` paper. """ - name = "meliusnet29" + name = "MeliusNet29" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet29, self).__init__("29", *args, **kwargs) @@ -205,7 +205,7 @@ class MeliusNet42(MeliusNet): ` paper. """ - name = "meliusnet42" + name = "MeliusNet42" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet42, self).__init__("42", *args, **kwargs) @@ -220,7 +220,7 @@ class MeliusNet59(MeliusNet): ` paper. """ - name = "meliusnet59" + name = "MeliusNet59" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet59, self).__init__("59", *args, **kwargs) @@ -231,11 +231,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class MeliusNetA(MeliusNet): - """MeliusNet-a model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + """MeliusNet-A model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ - name = "meliusneta" + name = "MeliusNetA" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetA, self).__init__("a", *args, **kwargs) @@ -246,11 +246,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class MeliusNetB(MeliusNet): - """MeliusNet-b model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + """MeliusNet-B model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ - name = "meliusnetb" + name = "MeliusNetB" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetB, self).__init__("b", *args, **kwargs) @@ -261,11 +261,11 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class MeliusNetC(MeliusNet): - """MeliusNet-c model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" + """MeliusNet-C model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ - name = "meliusnetc" + name = "MeliusNetC" def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetC, self).__init__("c", *args, **kwargs) diff --git a/bitorch/models/resnet.py b/bitorch/models/resnet.py index 0337efd..81bf157 100644 --- a/bitorch/models/resnet.py +++ b/bitorch/models/resnet.py @@ -449,7 +449,7 @@ def __init__( class Resnet(Model): - name = "resnet" + name = "Resnet" resnet_spec = { 18: ("basic_block", [2, 2, 2, 2], [64, 64, 128, 256, 512]), @@ -532,7 +532,7 @@ class Resnet18V1(Resnet): `_ paper. """ - name = "resnet18v1" + name = "Resnet18V1" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet18V1, self).__init__(1, 18, *args, **kwargs) @@ -547,7 +547,7 @@ class Resnet34V1(Resnet): `_ paper. """ - name = "resnet34v1" + name = "Resnet34V1" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet34V1, self).__init__(1, 34, *args, **kwargs) @@ -562,7 +562,7 @@ class Resnet50V1(Resnet): `_ paper. """ - name = "resnet50v1" + name = "Resnet50V1" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet50V1, self).__init__(1, 50, *args, **kwargs) @@ -577,7 +577,7 @@ class Resnet152V1(Resnet): `_ paper. """ - name = "resnet152v1" + name = "Resnet152V1" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet152V1, self).__init__(1, 152, *args, **kwargs) @@ -592,7 +592,7 @@ class Resnet18V2(Resnet): `_ paper. """ - name = "resnet18v2" + name = "Resnet18V2" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet18V2, self).__init__(2, 18, *args, **kwargs) @@ -607,7 +607,7 @@ class Resnet34V2(Resnet): `_ paper. """ - name = "resnet34v2" + name = "Resnet34V2" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet34V2, self).__init__(2, 34, *args, **kwargs) @@ -622,7 +622,7 @@ class Resnet50V2(Resnet): `_ paper. """ - name = "resnet50v2" + name = "Resnet50V2" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet50V2, self).__init__(2, 50, *args, **kwargs) @@ -637,7 +637,7 @@ class Resnet152V2(Resnet): `_ paper. """ - name = "resnet152v2" + name = "Resnet152V2" def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet152V2, self).__init__(2, 152, *args, **kwargs) diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 299acbb..9aa8c7d 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -198,7 +198,7 @@ def __init__( class ResnetE(Model): - name = "resnete" + name = "ResnetE" resnet_spec = { 18: ([2, 2, 2, 2], [64, 64, 128, 256, 512]), @@ -253,7 +253,7 @@ class ResnetE18(ResnetE): `_ paper. """ - name = "resnete18" + name = "ResnetE18" def __init__(self, *args: Any, **kwargs: Any) -> None: super(ResnetE18, self).__init__(18, *args, **kwargs) @@ -268,7 +268,7 @@ class ResnetE34(ResnetE): `_ paper. """ - name = "resnete34" + name = "ResnetE34" def __init__(self, *args: Any, **kwargs: Any) -> None: super(ResnetE34, self).__init__(34, *args, **kwargs) diff --git a/bitorch/util.py b/bitorch/util.py index 7a23fdc..86e95c3 100644 --- a/bitorch/util.py +++ b/bitorch/util.py @@ -33,11 +33,11 @@ def filter_fn(x: Any) -> bool: lookup = {} current_module = importlib.import_module(current_module_name) for class_name in class_strings: - # current_module = sys.modules.get(current_module_name, None) if not hasattr(current_module, class_name): continue class_ = getattr(current_module, class_name) if filter_fn(class_): - lookup[key_fn(class_)] = class_ + transformed_key = key_fn(class_) + lookup[transformed_key] = class_ return lookup diff --git a/tests/models/test_model_names.py b/tests/models/test_model_names.py new file mode 100644 index 0000000..e8d478b --- /dev/null +++ b/tests/models/test_model_names.py @@ -0,0 +1,13 @@ +import bitorch +from bitorch.models import model_from_name, model_names + + +def test_all_model_names(): + wrong_model_names = [] + for model_name in model_names(): + model = model_from_name(model_name) + assert model_from_name(model.name) == model + assert model_from_name(model.name.lower()) == model + if model.name != model.__name__: + wrong_model_names.append((model.name, model.__name__)) + assert len(wrong_model_names) == 0 From dd05c8255c64995b057d704bc7a890be8ebc13cf Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 11 Aug 2022 23:14:45 +0200 Subject: [PATCH 111/208] remove naming assert and add grouped_stem variant in common layers --- bitorch/models/common_layers.py | 20 ++++++++++++++++---- tests/models/test_models.py | 1 - 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index 97cb129..3dd8f92 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -5,16 +5,28 @@ def get_initial_layers(variant: str, input_channels: int, output_channels: int) -> List[nn.Module]: """Get commonly used layers to extract initial features from the image.""" layers: List[nn.Module] = [] - if variant == "thumbnail": - layers.append(nn.Conv2D(input_channels, kernel_size=3, strides=1, padding=1, use_bias=False)) if variant == "imagenet": layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False)) - layers.append(nn.BatchNorm2d(output_channels, momentum=0.9)) + elif variant == "grouped_stem": + stem_width = output_channels // 2 + + layers.append(nn.Conv2D(input_channels, stem_width, kernel_size=3, strides=2, padding=1, use_bias=False)) + layers.append(nn.BatchNorm2d(stem_width, momentum=0.9)) layers.append(nn.ReLU()) - layers.append(nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) + layers.append(nn.Conv2D(stem_width, stem_width, kernel_size=3, strides=1, padding=1, groups=4, use_bias=False)) + layers.append(nn.BatchNorm2d(stem_width, momentum=0.9)) + layers.append(nn.ReLU()) + layers.append( + nn.Conv2D(stem_width, stem_width * 2, kernel_size=3, strides=1, padding=1, groups=8, use_bias=False) + ) elif variant in ["mnist", "cifar10", "cifar100"]: layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=3, padding=1, bias=False)) else: raise ValueError(f"Unknown initial layers for dataset '{variant}'.") + if variant in ["imagenet", "grouped_stem"]: + layers.append(nn.BatchNorm2d(output_channels, momentum=0.9)) + layers.append(nn.ReLU()) + layers.append(nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) + return layers diff --git a/tests/models/test_models.py b/tests/models/test_models.py index ed1d06c..e563a70 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -64,7 +64,6 @@ @pytest.mark.parametrize("model_class, model_kwargs, datasets_to_test", TEST_INPUT_DATA) @pytest.mark.parametrize("dataset", ALL_DATASETS) def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: - assert models_by_name[model_class.name] is model_class if dataset not in datasets_to_test: pytest.skip(f"Model '{model_class.name}' does not need to work with the dataset '{dataset.name}'.") From c218951458abd9599cc4a5b3a861ea03b73ad903 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Thu, 11 Aug 2022 16:19:44 +0200 Subject: [PATCH 112/208] wip: rename args --- bitorch/models/densenet.py | 3 +- bitorch/models/meliusnet.py | 4 +- .../pytorch_lightning/utils/arg_parser.py | 45 ++++++++++++------- tests/test_argparse.py | 5 +-- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 7e92cfd..101bcb4 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -256,7 +256,7 @@ def __init__( @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--densenet-num-layers", + "--num-layers", type=int, choices=[None, 28, 37, 45], required=True, @@ -302,6 +302,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: + DenseNet.add_argparse_arguments(parser) parser.add_argument( "--block-config", type=str, diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 5f7a0d2..3eb1c2e 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -102,7 +102,7 @@ def __init__( @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--melius-num-layers", + "--num-layers", type=str, choices=[None, "22", "23", "29", "42", "59", "a", "b", "c"], required=True, @@ -146,7 +146,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - super().add_argparse_arguments(parser) # type: ignore + MeliusNet.add_argparse_arguments(parser) parser.add_argument( "--block-config", type=str, diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index af9875b..801980a 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -1,12 +1,13 @@ -from argparse import ArgumentParser +import argparse import sys -from typing import Tuple +from argparse import ArgumentParser +from typing import Tuple, List, Type, Any -from bitorch.models import model_from_name, model_names -from bitorch.datasets import dataset_names -from bitorch import add_config_args from pytorch_lightning import Trainer +from bitorch import add_config_args +from bitorch.datasets import dataset_names +from bitorch.models import model_from_name, model_names, Model from examples.pytorch_lightning.utils.teachers import available_teachers @@ -228,7 +229,7 @@ def add_dataset_args(parser: ArgumentParser) -> None: ) -def create_model_argparser(model_class: object) -> ArgumentParser: +def create_model_argparser(model_class: Type[Model]) -> ArgumentParser: """adds model specific cli arguments from model_class object Args: @@ -254,15 +255,14 @@ def help_in_args() -> bool: return False -def add_all_model_args(parser: ArgumentParser) -> None: - """iterates through all existent models and adds their specific cli args to parser - - Args: - parser (ArgumentParser): the main cli argument parser - """ +def get_all_model_parsers() -> List[ArgumentParser]: + """iterates through all existent models and adds a parser for each one""" + all_model_parsers = [] for model_name in model_names(): - model_group = parser.add_argument_group(model_name, f"parameters for {model_name} model") - model_from_name(model_name).add_argparse_arguments(model_group) # type: ignore + model_parser = argparse.ArgumentParser(f"Parser for arguments of {model_name}") + model_from_name(model_name).add_argparse_arguments(model_parser) # type: ignore + all_model_parsers.append(model_parser) + return all_model_parsers def add_regular_args(parser: ArgumentParser) -> None: @@ -299,18 +299,31 @@ def add_regular_args(parser: ArgumentParser) -> None: ) +class _CustomArgumentParser(ArgumentParser): + _sub_parsers: List[ArgumentParser] + + def print_help(self, *args: Any) -> None: + super(*args) + # if hasattr(self, "_sub_parsers"): + # for parser in self._sub_parsers: + # parser.print_help() + + def set_sub_parsers(self, sub_parsers: List[ArgumentParser]): + self._sub_parsers = sub_parsers + + def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: """creates a main argument parser for general options and a model parser for model specific options Returns: Tuple[ArgumentParser, ArgumentParser]: the main and model argument parser """ - parser = ArgumentParser(description="Bitorch Image Classification") + parser = _CustomArgumentParser(description="Bitorch Image Classification") add_regular_args(parser) if help_in_args(): - add_all_model_args(parser) + parser.set_sub_parsers(get_all_model_parsers()) args, _ = parser.parse_known_args() model_class = model_from_name(args.model) diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 5de14bb..1dd4bff 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -5,6 +5,5 @@ # this test checks for naming conflicts by adding all arguments to one parser def test_argparse(): arg_parser = pytest.importorskip("examples.pytorch_lightning.utils.arg_parser") - parser = ArgumentParser() - arg_parser.add_regular_args(parser) - arg_parser.add_all_model_args(parser) + parser = arg_parser.create_argparser() + # parser.parse_args(['main.py', '-h']) From f713770da95dfd9f49bf1f4a61d55db16c7f9855 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 12 Aug 2022 09:17:41 +0200 Subject: [PATCH 113/208] fix model parsing --- bitorch/models/base.py | 17 +++++- bitorch/models/densenet.py | 31 ++++------- bitorch/models/lenet.py | 2 +- bitorch/models/meliusnet.py | 50 ++++------------- bitorch/models/resnet.py | 54 ++++--------------- bitorch/models/resnet_e.py | 16 ++---- .../pytorch_lightning/utils/arg_parser.py | 50 ++++++++++------- 7 files changed, 80 insertions(+), 140 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index ac4b3ec..b48720f 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from typing import Union, Type +from typing import Union, Type, Any import torch from torch import nn @@ -84,3 +84,18 @@ def initialize(self) -> None: def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": return convert(self, new_mode, device, verbose) + + +class NoArgparseArgsMixin: + """ + Mixin for Models which subclass an existing Model, but do not have any argparse arguments anymore. + + By using this Mixin, there is no special Parser displayed for the class. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + @staticmethod + def add_argparse_arguments(parser: ArgumentParser) -> None: + pass diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 101bcb4..740fa69 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -6,7 +6,7 @@ from torch import nn from torch.nn import Module, ChannelShuffle -from .base import Model +from .base import Model, NoArgparseArgsMixin from bitorch.layers import QConv2d from bitorch.models.common_layers import get_initial_layers from bitorch.datasets.base import BasicDataset @@ -289,10 +289,9 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: class DenseNetFlex(DenseNet): - """DenseNet-Flex model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks" + """ + Flexible BinaryDenseNet model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks" ` paper. - - """ name = "DenseNetFlex" @@ -311,9 +310,9 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) -class DenseNet28(DenseNet): +class DenseNet28(NoArgparseArgsMixin, DenseNet): """ - DenseNet-28 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + BinaryDenseNet-28 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html @@ -324,14 +323,10 @@ class DenseNet28(DenseNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet28, self).__init__(28, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class DenseNet37(DenseNet): +class DenseNet37(NoArgparseArgsMixin, DenseNet): """ - DenseNet-37 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + BinaryDenseNet-37 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html @@ -342,14 +337,10 @@ class DenseNet37(DenseNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet37, self).__init__(37, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class DenseNet45(DenseNet): +class DenseNet45(NoArgparseArgsMixin, DenseNet): """ - DenseNet-45 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. + BinaryDenseNet-45 model from `"BinaryDenseNet: Developing an Architecture for Binary Neural Networks"` paper. .. _"BinaryDenseNet: Developing an Architecture for Binary Neural Networks": https://openaccess.thecvf.com/content_ICCVW_2019/html/NeurArch/Bethge_BinaryDenseNet_Developing_an_Architecture_for_Binary_Neural_Networks_ICCVW_2019_paper.html @@ -359,7 +350,3 @@ class DenseNet45(DenseNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(DenseNet45, self).__init__(45, *args, **kwargs) - - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass diff --git a/bitorch/models/lenet.py b/bitorch/models/lenet.py index f7f915d..4cc0fd3 100644 --- a/bitorch/models/lenet.py +++ b/bitorch/models/lenet.py @@ -84,4 +84,4 @@ def __init__(self, dataset: BasicDataset, lenet_version: int = 0) -> None: @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--lenet-version", type=int, default=0, help="choose a version of lenet") + parser.add_argument("--version", type=int, default=0, help="choose a version of lenet") diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index 3eb1c2e..df20c82 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -7,7 +7,7 @@ from torch.nn import Module from .densenet import BaseNetDense, DOWNSAMPLE_STRUCT, basedensenet_constructor -from .base import Model +from .base import Model, NoArgparseArgsMixin from bitorch.layers import QConv2d from bitorch.datasets import BasicDataset @@ -155,7 +155,7 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) -class MeliusNet22(MeliusNet): +class MeliusNet22(NoArgparseArgsMixin, MeliusNet): """MeliusNet-22 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -165,12 +165,8 @@ class MeliusNet22(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet22, self).__init__("22", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNet23(MeliusNet): +class MeliusNet23(NoArgparseArgsMixin, MeliusNet): """MeliusNet-23 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -180,12 +176,8 @@ class MeliusNet23(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet23, self).__init__("23", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNet29(MeliusNet): +class MeliusNet29(NoArgparseArgsMixin, MeliusNet): """MeliusNet-29 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -195,12 +187,8 @@ class MeliusNet29(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet29, self).__init__("29", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNet42(MeliusNet): +class MeliusNet42(NoArgparseArgsMixin, MeliusNet): """MeliusNet-42 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -210,12 +198,8 @@ class MeliusNet42(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet42, self).__init__("42", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNet59(MeliusNet): +class MeliusNet59(NoArgparseArgsMixin, MeliusNet): """MeliusNet-59 model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -225,12 +209,8 @@ class MeliusNet59(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNet59, self).__init__("59", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNetA(MeliusNet): +class MeliusNetA(NoArgparseArgsMixin, MeliusNet): """MeliusNet-A model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -240,12 +220,8 @@ class MeliusNetA(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetA, self).__init__("a", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNetB(MeliusNet): +class MeliusNetB(NoArgparseArgsMixin, MeliusNet): """MeliusNet-B model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -255,12 +231,8 @@ class MeliusNetB(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetB, self).__init__("b", *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class MeliusNetC(MeliusNet): +class MeliusNetC(NoArgparseArgsMixin, MeliusNet): """MeliusNet-C model from `"MeliusNet: Can Binary Neural Networks Achieve MobileNet-level Accuracy?" ` paper. """ @@ -269,7 +241,3 @@ class MeliusNetC(MeliusNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(MeliusNetC, self).__init__("c", *args, **kwargs) - - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass diff --git a/bitorch/models/resnet.py b/bitorch/models/resnet.py index 81bf157..ca1f21b 100644 --- a/bitorch/models/resnet.py +++ b/bitorch/models/resnet.py @@ -1,5 +1,5 @@ from bitorch.datasets.base import BasicDataset -from .base import Model +from .base import Model, NoArgparseArgsMixin from typing import List, Any from bitorch.layers import QConv2d_NoAct import torch @@ -512,14 +512,14 @@ def create_resnet( @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--resnet-version", + "--version", type=int, choices=[1, 2], required=True, help="version of resnet to be used", ) parser.add_argument( - "--resnet-num-layers", + "--num-layers", type=int, choices=[18, 34, 50, 152], required=True, @@ -527,7 +527,7 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) -class Resnet18V1(Resnet): +class Resnet18V1(NoArgparseArgsMixin, Resnet): """ResNet-18 V1 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -537,12 +537,8 @@ class Resnet18V1(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet18V1, self).__init__(1, 18, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet34V1(Resnet): +class Resnet34V1(NoArgparseArgsMixin, Resnet): """ResNet-34 V1 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -552,12 +548,8 @@ class Resnet34V1(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet34V1, self).__init__(1, 34, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet50V1(Resnet): +class Resnet50V1(NoArgparseArgsMixin, Resnet): """ResNet-50 V1 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -567,12 +559,8 @@ class Resnet50V1(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet50V1, self).__init__(1, 50, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet152V1(Resnet): +class Resnet152V1(NoArgparseArgsMixin, Resnet): """ResNet-152 V1 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -582,12 +570,8 @@ class Resnet152V1(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet152V1, self).__init__(1, 152, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet18V2(Resnet): +class Resnet18V2(NoArgparseArgsMixin, Resnet): """ResNet-18 V2 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -597,12 +581,8 @@ class Resnet18V2(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet18V2, self).__init__(2, 18, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet34V2(Resnet): +class Resnet34V2(NoArgparseArgsMixin, Resnet): """ResNet-34 V2 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -612,12 +592,8 @@ class Resnet34V2(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet34V2, self).__init__(2, 34, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet50V2(Resnet): +class Resnet50V2(NoArgparseArgsMixin, Resnet): """ResNet-50 V2 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -627,12 +603,8 @@ class Resnet50V2(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet50V2, self).__init__(2, 50, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class Resnet152V2(Resnet): +class Resnet152V2(NoArgparseArgsMixin, Resnet): """ResNet-152 V2 model from `"Deep Residual Learning for Image Recognition" `_ paper. """ @@ -641,7 +613,3 @@ class Resnet152V2(Resnet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(Resnet152V2, self).__init__(2, 152, *args, **kwargs) - - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 9aa8c7d..ee55b40 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -3,7 +3,7 @@ `_ paper. """ from bitorch.datasets.base import BasicDataset -from .base import Model +from .base import Model, NoArgparseArgsMixin from typing import List, Any import torch import argparse @@ -240,7 +240,7 @@ def create( @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--resnetE-num-layers", + "--num-layers", type=int, choices=[18, 34], required=True, @@ -248,7 +248,7 @@ def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: ) -class ResnetE18(ResnetE): +class ResnetE18(NoArgparseArgsMixin, ResnetE): """ResNetE-18 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" `_ paper. """ @@ -258,12 +258,8 @@ class ResnetE18(ResnetE): def __init__(self, *args: Any, **kwargs: Any) -> None: super(ResnetE18, self).__init__(18, *args, **kwargs) - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass - -class ResnetE34(ResnetE): +class ResnetE34(NoArgparseArgsMixin, ResnetE): """ResNetE-34 model from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" `_ paper. """ @@ -272,7 +268,3 @@ class ResnetE34(ResnetE): def __init__(self, *args: Any, **kwargs: Any) -> None: super(ResnetE34, self).__init__(34, *args, **kwargs) - - @staticmethod - def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - pass diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 801980a..63e480a 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -8,9 +8,24 @@ from bitorch import add_config_args from bitorch.datasets import dataset_names from bitorch.models import model_from_name, model_names, Model +from bitorch.models.base import NoArgparseArgsMixin from examples.pytorch_lightning.utils.teachers import available_teachers +class _HeadArgumentParser(ArgumentParser): + _informational_sub_parsers: List[ArgumentParser] + + def print_help(self, *args: Any) -> None: + super(*args).print_help(*args) + if hasattr(self, "_informational_sub_parsers"): + for parser in self._informational_sub_parsers: + print("\n") + parser.print_help() + + def add_informational_subparsers(self, sub_parsers: List[ArgumentParser]): + self._informational_sub_parsers = sub_parsers + + def add_logging_args(parser: ArgumentParser) -> None: """adds cli parameters for logging configuration @@ -238,7 +253,11 @@ def create_model_argparser(model_class: Type[Model]) -> ArgumentParser: Returns: ArgumentParser: cli argument parser """ - model_parser = ArgumentParser(add_help=False) + model_parser = argparse.ArgumentParser( + description=f"Additional arguments for {model_class.name} (--model {model_class.name.lower()})", + add_help=False, + usage=argparse.SUPPRESS, + ) model_class.add_argparse_arguments(model_parser) return model_parser @@ -255,12 +274,16 @@ def help_in_args() -> bool: return False -def get_all_model_parsers() -> List[ArgumentParser]: +def create_list_of_all_model_parsers() -> List[ArgumentParser]: """iterates through all existent models and adds a parser for each one""" all_model_parsers = [] for model_name in model_names(): - model_parser = argparse.ArgumentParser(f"Parser for arguments of {model_name}") - model_from_name(model_name).add_argparse_arguments(model_parser) # type: ignore + model_class = model_from_name(model_name) + if model_class.add_argparse_arguments == Model.add_argparse_arguments: + continue + if model_class.add_argparse_arguments == NoArgparseArgsMixin.add_argparse_arguments: + continue + model_parser = create_model_argparser(model_class) all_model_parsers.append(model_parser) return all_model_parsers @@ -295,35 +318,22 @@ def add_regular_args(parser: ArgumentParser) -> None: parser.add_argument( "--dev-run", action="store_true", - help="use only 1% of training/validation data for testing purposes", + help="use only 1%% of training/validation data for testing purposes", ) -class _CustomArgumentParser(ArgumentParser): - _sub_parsers: List[ArgumentParser] - - def print_help(self, *args: Any) -> None: - super(*args) - # if hasattr(self, "_sub_parsers"): - # for parser in self._sub_parsers: - # parser.print_help() - - def set_sub_parsers(self, sub_parsers: List[ArgumentParser]): - self._sub_parsers = sub_parsers - - def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: """creates a main argument parser for general options and a model parser for model specific options Returns: Tuple[ArgumentParser, ArgumentParser]: the main and model argument parser """ - parser = _CustomArgumentParser(description="Bitorch Image Classification") + parser = _HeadArgumentParser(description="Bitorch Image Classification") add_regular_args(parser) if help_in_args(): - parser.set_sub_parsers(get_all_model_parsers()) + parser.add_informational_subparsers(create_list_of_all_model_parsers()) args, _ = parser.parse_known_args() model_class = model_from_name(args.model) From 1a217d4acb074959cd438ea29490584e0ce7a2bb Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 12 Aug 2022 09:26:32 +0200 Subject: [PATCH 114/208] add lower to naming check --- tests/models/test_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index e563a70..fd39241 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -64,6 +64,7 @@ @pytest.mark.parametrize("model_class, model_kwargs, datasets_to_test", TEST_INPUT_DATA) @pytest.mark.parametrize("dataset", ALL_DATASETS) def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: + assert models_by_name[model_class.name.lower()] is model_class if dataset not in datasets_to_test: pytest.skip(f"Model '{model_class.name}' does not need to work with the dataset '{dataset.name}'.") From 2160cd0bc51b85a6f6a33765b2781d53adf95280 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 12 Aug 2022 09:40:53 +0200 Subject: [PATCH 115/208] fix test and formattings --- examples/pytorch_lightning/utils/arg_parser.py | 18 ++++++++++-------- tests/test_argparse.py | 8 ++++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 63e480a..1d51b8b 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -1,7 +1,7 @@ import argparse import sys from argparse import ArgumentParser -from typing import Tuple, List, Type, Any +from typing import Tuple, List, Type, Any, Optional, Sequence from pytorch_lightning import Trainer @@ -16,13 +16,13 @@ class _HeadArgumentParser(ArgumentParser): _informational_sub_parsers: List[ArgumentParser] def print_help(self, *args: Any) -> None: - super(*args).print_help(*args) + super().print_help(*args) if hasattr(self, "_informational_sub_parsers"): for parser in self._informational_sub_parsers: print("\n") parser.print_help() - def add_informational_subparsers(self, sub_parsers: List[ArgumentParser]): + def add_informational_subparsers(self, sub_parsers: List[ArgumentParser]) -> None: self._informational_sub_parsers = sub_parsers @@ -262,13 +262,15 @@ def create_model_argparser(model_class: Type[Model]) -> ArgumentParser: return model_parser -def help_in_args() -> bool: +def help_in_args(cmd_args: Optional[Sequence[str]] = None) -> bool: """determines if script was called with a --help or -h flag Returns: bool: True if help flag was set, False otherwise """ - passed_args = sys.argv[1:] + passed_args = cmd_args + if passed_args is None: + passed_args = sys.argv[1:] if "--help" in passed_args or "-h" in passed_args: return True return False @@ -322,7 +324,7 @@ def add_regular_args(parser: ArgumentParser) -> None: ) -def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: +def create_argparser(cmd_args: Optional[Sequence[str]] = None) -> Tuple[ArgumentParser, ArgumentParser]: """creates a main argument parser for general options and a model parser for model specific options Returns: @@ -332,9 +334,9 @@ def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: add_regular_args(parser) - if help_in_args(): + if help_in_args(cmd_args): parser.add_informational_subparsers(create_list_of_all_model_parsers()) - args, _ = parser.parse_known_args() + args, _ = parser.parse_known_args(cmd_args) model_class = model_from_name(args.model) model_parser = create_model_argparser(model_class) diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 1dd4bff..bfb0c43 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -1,9 +1,9 @@ import pytest -from argparse import ArgumentParser -# this test checks for naming conflicts by adding all arguments to one parser def test_argparse(): arg_parser = pytest.importorskip("examples.pytorch_lightning.utils.arg_parser") - parser = arg_parser.create_argparser() - # parser.parse_args(['main.py', '-h']) + with pytest.raises(SystemExit) as pytest_wrapped_e: + _ = arg_parser.create_argparser(["main.py", "-h"]) + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 From cca8eb8079f5e96bd1701ef911f02a49068b2a81 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 13:44:38 +0200 Subject: [PATCH 116/208] finished running version --- examples/dlrm/criteo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dlrm/criteo.py b/examples/dlrm/criteo.py index fdc429b..4289791 100644 --- a/examples/dlrm/criteo.py +++ b/examples/dlrm/criteo.py @@ -68,7 +68,7 @@ def get_dataset(self, download: bool = True) -> Dataset: raw_path=str(self.root_directory / "train.txt"), pro_data=str(self.root_directory / "preprocessed.npz"), memory_map=False, - dataset_multiprocessing=False, + dataset_multiprocessing=True, store_all_indices=True, ) gc.collect() From 4a2c2377d85d7944375fcca2abb54b0637b8afca Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 14:37:12 +0200 Subject: [PATCH 117/208] rebased branch from develop --- bitorch/datasets/__init__.py | 52 ------ bitorch/datasets/base.py | 169 ------------------ bitorch/datasets/cifar.py | 65 ------- bitorch/datasets/dummy_dataset.py | 23 --- bitorch/datasets/imagenet.py | 52 ------ bitorch/datasets/mnist.py | 23 --- bitorch/models/base.py | 8 +- bitorch/models/common_layers.py | 23 ++- bitorch/models/lenet.py | 28 ++- bitorch/models/resnet.py | 137 +++++++++----- bitorch/models/resnet_e.py | 62 ++++--- .../pytorch_lightning/image_classification.py | 8 +- .../pytorch_lightning/utils/arg_parser.py | 2 +- tests/models/test_model_conversion.py | 4 +- tests/models/test_models.py | 29 +-- 15 files changed, 196 insertions(+), 489 deletions(-) delete mode 100644 bitorch/datasets/__init__.py delete mode 100644 bitorch/datasets/base.py delete mode 100644 bitorch/datasets/cifar.py delete mode 100644 bitorch/datasets/dummy_dataset.py delete mode 100644 bitorch/datasets/imagenet.py delete mode 100644 bitorch/datasets/mnist.py diff --git a/bitorch/datasets/__init__.py b/bitorch/datasets/__init__.py deleted file mode 100644 index 67bc853..0000000 --- a/bitorch/datasets/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -This submodule contains data preparation code for some of the datasets used with our models, -i.e. MNIST, CIFAR 10 and 100 and ImageNet. -""" - -from typing import List, Type - -from .base import BasicDataset -from .cifar import CIFAR10, CIFAR100 -from .imagenet import ImageNet -from .mnist import MNIST -from ..util import build_lookup_dictionary - -__all__ = [ - "BasicDataset", - "dataset_from_name", - "dataset_names", - "MNIST", - "CIFAR10", - "CIFAR100", - "ImageNet", -] - - -datasets_by_name = build_lookup_dictionary(__name__, __all__, BasicDataset) - - -def dataset_from_name(name: str) -> Type[BasicDataset]: - """returns the dataset to which the name belongs to (name has to be the value of the datasets - name-attribute) - - Args: - name: name of the dataset - - Raises: - ValueError: raised if no dataset under that name was found - - Returns: - dataset: the dataset - """ - if name not in datasets_by_name: - raise ValueError(f"{name} dataset not found!") - return datasets_by_name[name] - - -def dataset_names() -> List[str]: - """getter for list of dataset names for argparse - - Returns: - List: the dataset names - """ - return list(datasets_by_name.keys()) diff --git a/bitorch/datasets/base.py b/bitorch/datasets/base.py deleted file mode 100644 index 9e144d4..0000000 --- a/bitorch/datasets/base.py +++ /dev/null @@ -1,169 +0,0 @@ -import logging -import os -from enum import Enum -from pathlib import Path -from typing import Optional, Tuple, Any - -import torch -from torch.utils.data import Dataset -from torchvision.transforms import transforms - -from bitorch.datasets.dummy_dataset import DummyDataset - - -class Augmentation(Enum): - NONE = -1 - DEFAULT = 0 - LOW = 1 - MEDIUM = 2 - HIGH = 3 - - @staticmethod - def from_string(level: str) -> "Augmentation": - return { - "none": Augmentation.NONE, - "default": Augmentation.DEFAULT, - "low": Augmentation.LOW, - "medium": Augmentation.MEDIUM, - "high": Augmentation.HIGH, - }[level] - - -class BasicDataset(Dataset): - name = "None" - num_classes = 0 - shape = (0, 0, 0, 0) - mean: Any = None - std_dev: Any = None - num_train_samples = 0 - num_val_samples = 0 - - def __init__( - self, - train: bool, - root_directory: str = None, - download: bool = False, - augmentation: Augmentation = Augmentation.DEFAULT, - ) -> None: - """initializes the dataset. - - Args: - train (bool): whether the train or test dataset is wanted - root_directory (str): path to main dataset storage directory - download (bool): whether train/test should be downloaded if it does not exist - augmentation (Augmentation): the level of augmentation (only for train dataset) - - Returns: - Dataset: the created test/train dataset - """ - super(BasicDataset, self).__init__() - self.is_train = train - self.augmentation_level = augmentation - self._download = download - self.root_directory = self.get_dataset_root_directory(root_directory) - self.dataset = self.get_dataset(download) - - @classmethod - def get_train_and_test( - cls, - root_directory: Optional[str] = None, - download: bool = False, - augmentation: Augmentation = Augmentation.DEFAULT, - ) -> Tuple["BasicDataset", "BasicDataset"]: - """creates a pair of train and test dataset. - - Returns: - Tuple: the train and test dataset - """ - return cls(True, root_directory, download, augmentation), cls(False, root_directory, download) - - @classmethod - def get_dummy_train_and_test_datasets(cls) -> Tuple[DummyDataset, DummyDataset]: - train_set = DummyDataset(cls.shape, cls.num_classes, cls.num_train_samples) # type: ignore - val_set = DummyDataset(cls.shape, cls.num_classes, cls.num_val_samples) # type: ignore - return train_set, val_set - - def get_dataset_root_directory(self, root_directory_argument: Optional[str]) -> Path: - """chooses the dataset root directory based on the passed argument or environment variables. - - Returns: - Tuple: the train and test dataset - """ - if root_directory_argument is not None: - return Path(root_directory_argument) - - environment_variable_name = f"{self.name.upper()}_HOME" - if os.environ.get(environment_variable_name) is not None: - return Path(os.environ.get(environment_variable_name)) # type: ignore - if os.environ.get("BITORCH_DATA_HOME") is not None: - return Path(os.environ.get("BITORCH_DATA_HOME")) / self.name # type: ignore - - environment_variable_hint = ( - f" To change this, set '{environment_variable_name}' or 'BITORCH_DATA_HOME' " - f"(in the latter case, the data resides in the folder '{self.name}' in BITORCH_DATA_HOME)." - f" Some datasets can be downloaded by adding the --download command line argument." - ) - if self._download: - logging.warning("Dataset is being downloaded to the directory './data'." + environment_variable_hint) - return Path("./data") - else: - raise ValueError(f"Dataset {self.name} not found." + environment_variable_hint) - - def get_dataset(self, download: bool) -> Dataset: - """creates the actual dataset - - Args: - download (bool): toggles if train/test shall be downloaded if possible - - Raises: - NotImplementedError: thrown, because this method needs to be overwritten by subclasses - - Returns: - Dataset: the created test/train dataset - """ - raise NotImplementedError() - - def get_transform(self) -> Any: - if self.is_train: - return self.train_transform(self.augmentation_level) - return self.test_transform() - - @classmethod - def test_transform(cls) -> Any: - """get the transform for the test data. - - Returns: - transform: the transform pipeline - """ - return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) - - @classmethod - def train_transform(cls, augmentation: Augmentation = Augmentation.DEFAULT) -> Any: - """get the transform for the training data (should consider the current augmentation_level). - - Returns: - transform: the transform pipeline - """ - return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) - - @classmethod - def get_normalize_transform(cls) -> transforms.Normalize: - return transforms.Normalize(cls.mean, cls.std_dev) - - def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]: # type: ignore - """returns the item at the given index of the dataset. - - Args: - index (int): requested index - - Returns: - Tuple[torch.Tensor, torch.Tensor]: data and label at the specified index - """ - return self.dataset[index] - - def __len__(self) -> int: - return len(self.dataset) # type: ignore - - def num_samples(self) -> int: - """returns the (theoretical) dataset size.""" - return self.num_train_samples if self.is_train else self.num_val_samples diff --git a/bitorch/datasets/cifar.py b/bitorch/datasets/cifar.py deleted file mode 100644 index 4e4264a..0000000 --- a/bitorch/datasets/cifar.py +++ /dev/null @@ -1,65 +0,0 @@ -from abc import ABC - -from torch.utils.data import Dataset -from torchvision.datasets import cifar -from torchvision.transforms import transforms - -from .base import BasicDataset, Augmentation - -__all__ = ["CIFAR10", "CIFAR100"] - - -class CIFAR(BasicDataset, ABC): - shape = (1, 3, 32, 32) - num_train_samples = 50000 - num_val_samples = 10000 - - @classmethod - def train_transform(cls, augmentation: Augmentation = Augmentation.DEFAULT) -> transforms.Compose: - return transforms.Compose( - [ - transforms.RandomCrop(32, padding=4), - transforms.RandomHorizontalFlip(), - transforms.ToTensor(), - cls.get_normalize_transform(), - ] - ) - - @classmethod - def test_transform(cls) -> transforms.Compose: - return transforms.Compose( - [ - transforms.ToTensor(), - cls.get_normalize_transform(), - ] - ) - - -class CIFAR10(CIFAR): - name = "cifar10" - num_classes = 10 - mean = (0.4914, 0.4822, 0.4465) - std_dev = (0.2023, 0.1994, 0.2010) - - def get_dataset(self, download: bool = True) -> Dataset: - return cifar.CIFAR10( - root=self.root_directory, - train=self.is_train, - transform=self.get_transform(), - download=download, - ) - - -class CIFAR100(CIFAR): - name = "cifar100" - num_classes = 100 - mean = (0.507, 0.487, 0.441) - std_dev = (0.267, 0.256, 0.276) - - def get_dataset(self, download: bool = True) -> Dataset: - return cifar.CIFAR100( - root=self.root_directory, - train=self.is_train, - transform=self.get_transform(), - download=download, - ) diff --git a/bitorch/datasets/dummy_dataset.py b/bitorch/datasets/dummy_dataset.py deleted file mode 100644 index 91b5524..0000000 --- a/bitorch/datasets/dummy_dataset.py +++ /dev/null @@ -1,23 +0,0 @@ -from torch.utils.data import Dataset -import torch -from typing import Tuple - - -class DummyDataset(Dataset): - """An iterator that produces repeated dummy data. - Args: - data_sample: a data sample that should be produced at each step. - batch_size: the batch size for storing. - sample_count: number of `data` samples in the dummy dataset. - """ - - def __init__(self, data_shape: torch.Size, num_classes: int, sample_count: int) -> None: - self._data_sample = torch.zeros(data_shape) - self._class_sample = torch.zeros((num_classes,), dtype=torch.int64) - self._sample_count = sample_count - - def __len__(self) -> int: - return self._sample_count - - def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: - return self._data_sample, self._class_sample diff --git a/bitorch/datasets/imagenet.py b/bitorch/datasets/imagenet.py deleted file mode 100644 index 2eaa2b7..0000000 --- a/bitorch/datasets/imagenet.py +++ /dev/null @@ -1,52 +0,0 @@ -from pathlib import Path - -from torch.utils.data import Dataset -from torchvision import transforms -from torchvision.datasets import ImageFolder - -from .base import BasicDataset, Augmentation - - -class ImageNet(BasicDataset): - name = "imagenet" - num_classes = 1000 - shape = (1, 3, 224, 224) - mean = (0.485, 0.456, 0.406) - std_dev = (0.229, 0.224, 0.255) - num_train_samples = 1281167 - num_val_samples = 50000 - - def get_data_dir(self) -> Path: - split = "train" if self.is_train else "val" - directory = self.root_directory / split - return directory - - def get_dataset(self, download: bool) -> Dataset: - directory = self.get_data_dir() - print("got directory for imagenet:", directory) - if download and not directory.is_dir(): - raise RuntimeError("ImageNet dataset must be downloaded and prepared manually.") - return ImageFolder(directory, transform=self.get_transform()) - - @classmethod - def train_transform(cls, augmentation: Augmentation = Augmentation.DEFAULT) -> transforms.Compose: - crop_scale = 0.08 - return transforms.Compose( - [ - transforms.RandomResizedCrop(224, scale=(crop_scale, 1.0)), - transforms.RandomHorizontalFlip(), - transforms.ToTensor(), - cls.get_normalize_transform(), - ] - ) - - @classmethod - def test_transform(cls) -> transforms.Compose: - return transforms.Compose( - [ - transforms.Resize(256), - transforms.CenterCrop(224), - transforms.ToTensor(), - cls.get_normalize_transform(), - ] - ) diff --git a/bitorch/datasets/mnist.py b/bitorch/datasets/mnist.py deleted file mode 100644 index fde7f8e..0000000 --- a/bitorch/datasets/mnist.py +++ /dev/null @@ -1,23 +0,0 @@ -from torch.utils.data import Dataset -from torchvision.datasets import mnist - -from .base import BasicDataset - - -class MNIST(BasicDataset): - name = "mnist" - num_classes = 10 - shape = (1, 1, 28, 28) - - mean = (0.1307,) - std_dev = (0.3081,) - num_train_samples = 60000 - num_val_samples = 10000 - - def get_dataset(self, download: bool = True) -> Dataset: - return mnist.MNIST( - root=self.root_directory, - train=self.is_train, - transform=self.get_transform(), - download=download, - ) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 8da4e16..2b4edd0 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,11 +1,10 @@ from argparse import ArgumentParser -from typing import Union, Type +from typing import List import torch from torch import nn from bitorch import RuntimeMode -from bitorch.datasets.base import BasicDataset from bitorch.layers import convert from bitorch.layers.qconv1d import QConv1dBase, QConv1d_NoAct from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct @@ -17,10 +16,11 @@ class Model(nn.Module): name = "None" - def __init__(self, dataset: Union[BasicDataset, Type[BasicDataset]]) -> None: + def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() self._model = nn.Module() - self._dataset = dataset + self._input_shape = input_shape + self._num_classes = num_classes @staticmethod def add_argparse_arguments(parser: ArgumentParser) -> None: diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index a4c938e..72ee3ac 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -1,18 +1,25 @@ -from typing import List +from typing import List, Optional from torch import nn -def get_initial_layers(variant: str, input_channels: int, output_channels: int) -> List[nn.Module]: - """Get commonly used layers to extract initial features from the image.""" +def get_initial_layers(variant: Optional[List[int]], input_channels: int, output_channels: int) -> List[nn.Module]: + """returns the initial layers for the given variant""" layers: List[nn.Module] = [] - if variant == "imagenet": - layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False)) + if variant == (224, 224): # imagenet + layers.append( + nn.Conv2d( + input_channels, + output_channels, + kernel_size=7, + stride=2, + padding=3, + bias=False, + ) + ) layers.append(nn.BatchNorm2d(output_channels, momentum=0.9)) layers.append(nn.ReLU()) layers.append(nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) - elif variant in ["mnist", "cifar10", "cifar100"]: - layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=3, padding=1, bias=False)) else: - raise ValueError(f"Unknown initial layers for dataset '{variant}'.") + layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=3, padding=1, bias=False)) return layers diff --git a/bitorch/models/lenet.py b/bitorch/models/lenet.py index 7dd2047..feae37f 100644 --- a/bitorch/models/lenet.py +++ b/bitorch/models/lenet.py @@ -1,6 +1,6 @@ import argparse +from typing import List from bitorch.layers.debug_layers import ShapePrintDebug -from bitorch.datasets.base import BasicDataset from bitorch.layers import QLinear, QConv2d, QActivation from torch import nn from .base import Model @@ -41,22 +41,32 @@ def generate_quant_model( ShapePrintDebug(), nn.Flatten(), QActivation(activation=input_quant_2), - QLinear(self.num_channels_conv * 4 * 4, self.num_fc, weight_quantization=weight_quant_2), + QLinear( + self.num_channels_conv * 4 * 4, + self.num_fc, + weight_quantization=weight_quant_2, + ), nn.BatchNorm1d(self.num_fc), self.activation_function(), nn.Linear(self.num_fc, self.num_output), ) return model - def __init__(self, dataset: BasicDataset, lenet_version: int = 0) -> None: - """builds the model, depending on mode in either quantized or full_precision mode + def __init__(self, input_shape: List[int], num_classes: int = 0, lenet_version: int = 0) -> None: + """builds the model depending on mode in either quantized or full_precision mode Args: - lenet_quantized (bool, optional): toggles use of quantized version of lenet. Default is False. + input_shape (List[int]): input shape of images + num_classes (int, optional): number of output classes. Defaults to None. + lenet_version (int, optional): lenet version. if version outside of [0, 3], the full precision version is used. Defaults to 0. + + Raises: + ValueError: thrown if num classes is none """ - super(LeNet, self).__init__(dataset) - self.input_channels = dataset.shape[1] - self.num_output = dataset.num_classes + super(LeNet, self).__init__(input_shape, num_classes) + self.input_channels = self._input_shape[1] + self.num_output = self._num_classes + if lenet_version == 0: self._model = self.generate_quant_model("sign", "sign") elif lenet_version == 1: @@ -84,4 +94,4 @@ def __init__(self, dataset: BasicDataset, lenet_version: int = 0) -> None: @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--lenet-version", type=int, default=0, help="choose a version of lenet") + parser.add_argument("--lenet-version", type=int, default=0, help="choses a verion of lenet") diff --git a/bitorch/models/resnet.py b/bitorch/models/resnet.py index 0337efd..7c9b48e 100644 --- a/bitorch/models/resnet.py +++ b/bitorch/models/resnet.py @@ -1,4 +1,3 @@ -from bitorch.datasets.base import BasicDataset from .base import Model from typing import List, Any from bitorch.layers import QConv2d_NoAct @@ -44,7 +43,13 @@ def _build_downsampling(self) -> nn.Sequential: nn.Sequential: the downsampling model """ return nn.Sequential( - QConv2d(self.in_channels, self.out_channels, kernel_size=1, stride=self.stride, padding=0), + QConv2d( + self.in_channels, + self.out_channels, + kernel_size=1, + stride=self.stride, + padding=0, + ), nn.BatchNorm2d(self.out_channels), ) @@ -56,7 +61,13 @@ def _build_body(self) -> nn.Sequential: nn.Sequential: the basic building block body model """ return nn.Sequential( - QConv2d(self.in_channels, self.out_channels, kernel_size=3, stride=self.stride, padding=1), + QConv2d( + self.in_channels, + self.out_channels, + kernel_size=3, + stride=self.stride, + padding=1, + ), nn.BatchNorm2d(self.out_channels), QConv2d(self.out_channels, self.out_channels, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(self.out_channels), @@ -112,7 +123,12 @@ def _build_downsampling(self) -> nn.Sequential: """ return nn.Sequential( QConv2d_NoAct( - self.in_channels, self.out_channels, kernel_size=1, stride=self.stride, padding=0, bias=False + self.in_channels, + self.out_channels, + kernel_size=1, + stride=self.stride, + padding=0, + bias=False, ), nn.BatchNorm2d(self.out_channels), ) @@ -124,10 +140,21 @@ def _build_body(self) -> nn.Sequential: nn.Sequential: the bottleneck body model """ return nn.Sequential( - QConv2d_NoAct(self.in_channels, self.out_channels // 4, kernel_size=1, stride=self.stride), + QConv2d_NoAct( + self.in_channels, + self.out_channels // 4, + kernel_size=1, + stride=self.stride, + ), nn.BatchNorm2d(self.out_channels // 4), nn.ReLU(), - QConv2d_NoAct(self.out_channels // 4, self.out_channels // 4, kernel_size=3, stride=1, padding=1), + QConv2d_NoAct( + self.out_channels // 4, + self.out_channels // 4, + kernel_size=3, + stride=1, + padding=1, + ), nn.BatchNorm2d(self.out_channels // 4), nn.ReLU(), QConv2d_NoAct(self.out_channels // 4, self.out_channels, kernel_size=1, stride=1), @@ -183,7 +210,13 @@ def _build_downsampling(self) -> nn.Module: Returns: QConv2d: the downsampling convolution layer """ - return QConv2d(self.in_channels, self.out_channels, kernel_size=1, stride=self.stride, padding=0) + return QConv2d( + self.in_channels, + self.out_channels, + kernel_size=1, + stride=self.stride, + padding=0, + ) def _build_body(self) -> nn.Sequential: """builds body of building block. Check referenced paper for more details. @@ -192,7 +225,13 @@ def _build_body(self) -> nn.Sequential: nn.Sequential: the bottleneck body model """ return nn.Sequential( - QConv2d(self.in_channels, self.out_channels, kernel_size=3, stride=self.stride, padding=1), + QConv2d( + self.in_channels, + self.out_channels, + kernel_size=3, + stride=self.stride, + padding=1, + ), nn.BatchNorm2d(self.out_channels), QConv2d(self.out_channels, self.out_channels, kernel_size=3, stride=1, padding=1), ) @@ -242,7 +281,13 @@ def _build_downsampling(self) -> nn.Module: Returns: QConv2d: the downsampling convolution layer """ - return QConv2d_NoAct(self.in_channels, self.out_channels, kernel_size=1, stride=self.stride, bias=False) + return QConv2d_NoAct( + self.in_channels, + self.out_channels, + kernel_size=1, + stride=self.stride, + bias=False, + ) def _build_body(self) -> nn.Sequential: """builds body of building block. Check referenced paper for more details. @@ -251,10 +296,21 @@ def _build_body(self) -> nn.Sequential: nn.Sequential: the bottleneck body model """ return nn.Sequential( - QConv2d_NoAct(self.in_channels, self.out_channels // 4, kernel_size=1, stride=self.stride), + QConv2d_NoAct( + self.in_channels, + self.out_channels // 4, + kernel_size=1, + stride=self.stride, + ), nn.BatchNorm2d(self.out_channels // 4), nn.ReLU(), - QConv2d_NoAct(self.out_channels // 4, self.out_channels // 4, kernel_size=3, stride=1, padding=1), + QConv2d_NoAct( + self.out_channels // 4, + self.out_channels // 4, + kernel_size=3, + stride=1, + padding=1, + ), nn.BatchNorm2d(self.out_channels // 4), nn.ReLU(), QConv2d_NoAct(self.out_channels // 4, self.out_channels, kernel_size=1, stride=1), @@ -292,7 +348,14 @@ def __init__(self, classes: int, channels: list) -> None: self.features = nn.Sequential() self.output_layer = nn.Linear(channels[-1], classes) - def make_layer(self, block: Module, layers: int, in_channels: int, out_channels: int, stride: int) -> nn.Sequential: + def make_layer( + self, + block: Module, + layers: int, + in_channels: int, + out_channels: int, + stride: int, + ) -> nn.Sequential: """builds a layer by stacking blocks in a sequential models. Args: @@ -354,7 +417,7 @@ def __init__( layers: list, channels: list, classes: int, - initial_layers: str = "imagenet", + image_resolution: List[int] = None, image_channels: int = 3, ) -> None: """Creates ResNetV1 model. @@ -365,8 +428,8 @@ def __init__( channels (list): channel num used for input/output channel size of layers. there must always be one more channels than there are layers. classes (int): number of output classes - initial_layers (str, optional): name of set for initial layers. refer to common_layers.py. - Defaults to "imagenet". + image_resolution (List[int], optional): resolution of input image. refer to common_layers.py. + Defaults to None. image_channels (int, optional): input channels of images. Defaults to 3. Raises: @@ -380,7 +443,7 @@ def __init__( feature_layers: List[nn.Module] = [] feature_layers.append(nn.BatchNorm2d(image_channels)) - feature_layers.extend(get_initial_layers(initial_layers, image_channels, channels[0])) + feature_layers.extend(get_initial_layers(image_resolution, image_channels, channels[0])) feature_layers.append(nn.BatchNorm2d(channels[0])) feature_layers.extend(self.make_feature_layers(block, layers, channels)) @@ -404,7 +467,7 @@ def __init__( layers: list, channels: list, classes: int = 1000, - initial_layers: str = "imagenet", + image_resolution: List[int] = None, image_channels: int = 3, ) -> None: """Creates ResNetV2 model. @@ -415,8 +478,8 @@ def __init__( channels (list): channel num used for input/output channel size of layers. there must always be one more channels than there are layers. classes (int): number of output classes - initial_layers (str, optional): name of set for initial layers. refer to common_layers.py. - Defaults to "imagenet". + image_resolution (List[int], optional): resolution of input image. refer to common_layers.py. + Defaults to None. image_channels (int, optional): input channels of images. Defaults to 3. Raises: @@ -430,7 +493,7 @@ def __init__( feature_layers: List[nn.Module] = [] feature_layers.append(nn.BatchNorm2d(image_channels)) - feature_layers.extend(get_initial_layers(initial_layers, image_channels, channels[0])) + feature_layers.extend(get_initial_layers(image_resolution, image_channels, channels[0])) feature_layers.extend(self.make_feature_layers(block, layers, channels)) @@ -464,33 +527,23 @@ class Resnet(Model): {"basic_block": BasicBlockV2, "bottle_neck": BottleneckV2}, ] - def __init__(self, resnet_version: int, resnet_num_layers: int, dataset: BasicDataset) -> None: - super(Resnet, self).__init__(dataset) - self._model = self.create_resnet( - resnet_version, - resnet_num_layers, - self._dataset.num_classes, - self._dataset.name, - self._dataset.shape[1], - ) + def __init__( + self, + resnet_version: int, + resnet_num_layers: int, + input_shape: List[int], + num_classes: int = 0, + ) -> None: + super(Resnet, self).__init__(input_shape, num_classes) + self._model = self.create_resnet(resnet_version, resnet_num_layers) logging.info(f"building Resnetv{str(resnet_version)} with {str(resnet_num_layers)} layers...") - def create_resnet( - self, - version: int, - num_layers: int, - classes: int = 1000, - initial_layers: str = "imagenet", - image_channels: int = 3, - ) -> Module: + def create_resnet(self, version: int, num_layers: int) -> Module: """Creates a resnet complying to given version and layer number. Args: version (int): version of resnet to be used. availavle versions are 1 or 2 num_layers (int): number of layers to be build. - classes (int, optional): number of output classes. Defaults to 1000. - initial_layers (str, optional): name of set of initial layers to be used. Defaults to "imagenet". - image_channels (int, optional): number of channels of input images. Defaults to 3. Raises: ValueError: raised if no resnet specification for given num_layers is listed in the resnet_spec dict above @@ -504,10 +557,12 @@ def create_resnet( if version not in [1, 2]: raise ValueError(f"invalid resnet version {version}, only 1 or 2 allowed") + image_channels = self._input_shape[1] + image_resolution = self._input_shape[-2:] block_type, layers, channels = self.resnet_spec[num_layers] resnet = self.resnet_net_versions[version - 1] block = self.resnet_block_versions[version - 1][block_type] - return resnet(block, layers, channels, classes, initial_layers, image_channels) + return resnet(block, layers, channels, self._num_classes, image_resolution, image_channels) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 299acbb..164a92e 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -2,7 +2,6 @@ Resnet_E implementation from `"Back to Simplicity: How to Train Accurate BNNs from Scratch?" `_ paper. """ -from bitorch.datasets.base import BasicDataset from .base import Model from typing import List, Any import torch @@ -49,19 +48,33 @@ def _build_downsampling(self) -> nn.Sequential: """ return nn.Sequential( nn.AvgPool2d(kernel_size=2, stride=self.stride), - nn.Conv2d(self.in_channels, self.out_channels, kernel_size=1, stride=1, padding=0, bias=False), + nn.Conv2d( + self.in_channels, + self.out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=False, + ), nn.BatchNorm2d(self.out_channels, momentum=0.9), ) def _build_body(self) -> nn.Sequential: - """Builds the body of a building block, i.e. two binary convolutions with BatchNorms in between. - Check the referenced paper for more details. + """builds body of building block, i.e. two binary convolutions with batchnorms in between. Check referenced paper for + more details. Returns: nn.Sequential: the basic building block body model """ return nn.Sequential( - QConv2d(self.in_channels, self.out_channels, kernel_size=3, stride=self.stride, padding=1, bias=False), + QConv2d( + self.in_channels, + self.out_channels, + kernel_size=3, + stride=self.stride, + padding=1, + bias=False, + ), nn.BatchNorm2d(self.out_channels, momentum=0.9), ) @@ -108,7 +121,7 @@ def make_layer(self, layers: int, in_channels: int, out_channels: int, stride: i Returns: nn.Sequential: the model containing the building blocks """ - # this trick adds shortcut connections between original resnet blocks + # this tricks adds shortcut connections between original resnet blocks # we multiple number of blocks by 2, but add only one layer instead of two in each block layers = layers * 2 @@ -155,7 +168,12 @@ class _ResnetE(SpecificResnetE): """ def __init__( - self, layers: list, channels: list, classes: int, initial_layers: str = "imagenet", image_channels: int = 3 + self, + layers: list, + channels: list, + classes: int, + image_resolution: List[int] = None, + image_channels: int = 3, ) -> None: """Creates ResNetE model. @@ -164,8 +182,8 @@ def __init__( channels (list): channel num used for input/output channel size of layers. there must always be one more channels than there are layers. classes (int): number of output classes - initial_layers (str, optional): name of set for initial layers. refer to common_layers.py. - Defaults to "imagenet". + image_resolution (List[int], optional): resolution of input image. refer to common_layers.py. + Defaults to None. image_channels (int, optional): input channels of images. Defaults to 3. Raises: @@ -179,7 +197,7 @@ def __init__( feature_layers: List[nn.Module] = [] # feature_layers.append(nn.BatchNorm2d(image_channels, eps=2e-5, momentum=0.9)) - feature_layers.extend(get_initial_layers(initial_layers, image_channels, channels[0])) + feature_layers.extend(get_initial_layers(image_resolution, image_channels, channels[0])) feature_layers.append(nn.BatchNorm2d(channels[0], momentum=0.9)) feature_layers.extend(self.make_feature_layers(layers, channels)) @@ -205,24 +223,16 @@ class ResnetE(Model): 34: ([3, 4, 6, 3], [64, 64, 128, 256, 512]), } - def __init__(self, resnete_num_layers: int, dataset: BasicDataset) -> None: - super(ResnetE, self).__init__(dataset) - self._model = self.create( - resnete_num_layers, self._dataset.num_classes, self._dataset.name, self._dataset.shape[1] - ) + def __init__(self, resnete_num_layers: int, input_shape: List[int], num_classes: int = 0) -> None: + super(ResnetE, self).__init__(input_shape, num_classes) + self._model = self.create(resnete_num_layers) logging.info(f"building ResnetE with {str(resnete_num_layers)} layers...") - @classmethod - def create( - cls, num_layers: int, classes: int = 1000, initial_layers: str = "imagenet", image_channels: int = 3 - ) -> nn.Module: + def create(self, num_layers: int) -> nn.Module: """Creates a ResNetE complying to given layer number. Args: num_layers (int): number of layers to be build. - classes (int, optional): number of output classes. Defaults to 1000. - initial_layers (str, optional): name of set of initial layers to be used. Defaults to "imagenet". - image_channels (int, optional): number of channels of input images. Defaults to 3. Raises: ValueError: raised if no resnet specification for given num_layers is listed in the resnet_spec dict above @@ -230,12 +240,14 @@ def create( Returns: Module: resnetE model """ - if num_layers not in cls.resnet_spec: + if num_layers not in self.resnet_spec: raise ValueError(f"No resnet spec for {num_layers} available!") - layers, channels = cls.resnet_spec[num_layers] + layers, channels = self.resnet_spec[num_layers] + image_channels = self._input_shape[1] + image_resolution = self._input_shape[-2:] - return _ResnetE(layers, channels, classes, initial_layers, image_channels) + return _ResnetE(layers, channels, self._num_classes, image_resolution, image_channels) @staticmethod def add_argparse_arguments(parser: argparse.ArgumentParser) -> None: diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index b406c2c..43b2dc9 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -25,8 +25,7 @@ import bitorch from bitorch import apply_args_to_configuration, RuntimeMode -from bitorch.datasets import dataset_from_name -from bitorch.datasets.base import Augmentation +from datasets import dataset_from_name from bitorch.models import model_from_name from bitorch.quantizations import Quantization from examples.pytorch_lightning.utils.callbacks import ProgressiveSignScalerCallback @@ -102,7 +101,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - model = model_from_name(args.model)(**model_kwargs, dataset=dataset) # type: ignore + model = model_from_name(args.model)(**model_kwargs, input_shape=dataset.shape, num_classes=dataset.num_classes) # type: ignore model.initialize() wrapper_class: Type[ModelWrapper] = ModelWrapper @@ -132,7 +131,6 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: limit_train_batches=0.01 if args.dev_run else None, limit_val_batches=0.01 if args.dev_run else None, ) - augmentation_level = Augmentation.from_string(args.augmentation) if args.dev_run: logger.info("This run only uses 1 % of training and validation data (--dev-run)!") logger.info(f"model: {args.model}") @@ -145,7 +143,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: else: logger.info(f"dataset: {dataset.name}") train_dataset, test_dataset = dataset.get_train_and_test( # type: ignore - root_directory=args.dataset_dir, download=args.download, augmentation=augmentation_level + root_directory=args.dataset_dir, download=args.download ) train_loader = DataLoader( train_dataset, diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index af9875b..bc971cf 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -3,7 +3,7 @@ from typing import Tuple from bitorch.models import model_from_name, model_names -from bitorch.datasets import dataset_names +from ..datasets import dataset_names from bitorch import add_config_args from pytorch_lightning import Trainer diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 98ae191..05b41de 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -8,7 +8,7 @@ import bitorch import bitorch.runtime_mode from bitorch import RuntimeMode -from bitorch.datasets import MNIST +from examples.pytorch_lightning.datasets import MNIST from bitorch.layers import QConv2d, QLinear from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin from bitorch.layers.extensions import LayerRecipe @@ -29,7 +29,7 @@ class _TestModel(Model): def __init__(self): - super().__init__(dataset=MNIST) + super().__init__(input_shape=MNIST.shape, num_classes=MNIST.num_classes) self.q_conv2d = QConv2d(1, 32, 3, 1, 1) self.q_linear = QLinear(784, 64) self._model = nn.Sequential( diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 0a89a81..73e176b 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -1,5 +1,3 @@ -from bitorch.datasets import MNIST, CIFAR10, CIFAR100, ImageNet - from bitorch.models import ( models_by_name, LeNet, @@ -19,11 +17,21 @@ import pytest import itertools -ALL_DATASETS = [MNIST, CIFAR10, CIFAR100, ImageNet] -RGB_DATASETS = [CIFAR10, CIFAR100, ImageNet] + +MNIST = [(1, 1, 28, 28), 10, "MNIST"] +CIFAR10 = [(1, 3, 32, 32), 10, "CIFAR10"] +CIFAR100 = [(1, 3, 32, 32), 100, "CIFAR100"] +IMAGENET = [(1, 3, 224, 224), 1000, "IMAGENET"] + +ALL_DATASETS = [MNIST, CIFAR10, CIFAR100, IMAGENET] +RGB_DATASETS = [CIFAR10, CIFAR100, IMAGENET] TEST_INPUT_DATA = [ - [Resnet, {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, ALL_DATASETS], + [ + Resnet, + {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, + ALL_DATASETS, + ], [Resnet18V1, {}, ALL_DATASETS], [Resnet34V1, {}, ALL_DATASETS], [Resnet50V1, {}, ALL_DATASETS], @@ -42,7 +50,7 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: assert models_by_name[model_class.name] is model_class if dataset not in datasets_to_test: - pytest.skip(f"Model '{model_class.name}' does not need to work with the dataset '{dataset.name}'.") + pytest.skip(f"Model '{model_class.name}' does not need to work with the dataset '{dataset[2]}'.") all_model_kwargs_combinations = [ dict(zip(model_kwargs.keys(), combination)) for combination in itertools.product(*model_kwargs.values()) @@ -50,15 +58,16 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: for combination in all_model_kwargs_combinations: batch_sizes_to_test = [2, 10] - if dataset is ImageNet: + if dataset is IMAGENET: batch_sizes_to_test = [1, 2] for batch_size in batch_sizes_to_test: - input_shape = list(dataset.shape) + input_shape = list(dataset[0]) input_shape[0] = batch_size - model = model_class(dataset=dataset, **combination) + model = model_class(input_shape=dataset[0], num_classes=dataset[1], **combination) input_values = torch.Tensor(np.random.uniform(0, 1.0, input_shape)) output = model(input_values) assert torch.equal( - torch.as_tensor(output.shape), torch.Tensor([input_shape[0], dataset.num_classes]).long() + torch.as_tensor(output.shape), + torch.Tensor([input_shape[0], dataset[1]]).long(), ) From f9cdbf941c0049e09086292870c24179a0640f63 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 14:37:31 +0200 Subject: [PATCH 118/208] added dataset files --- .../pytorch_lightning/datasets/__init__.py | 49 +++++++ examples/pytorch_lightning/datasets/base.py | 137 ++++++++++++++++++ examples/pytorch_lightning/datasets/cifar.py | 65 +++++++++ .../datasets/dummy_dataset.py | 23 +++ .../pytorch_lightning/datasets/imagenet.py | 52 +++++++ examples/pytorch_lightning/datasets/mnist.py | 23 +++ 6 files changed, 349 insertions(+) create mode 100644 examples/pytorch_lightning/datasets/__init__.py create mode 100644 examples/pytorch_lightning/datasets/base.py create mode 100644 examples/pytorch_lightning/datasets/cifar.py create mode 100644 examples/pytorch_lightning/datasets/dummy_dataset.py create mode 100644 examples/pytorch_lightning/datasets/imagenet.py create mode 100644 examples/pytorch_lightning/datasets/mnist.py diff --git a/examples/pytorch_lightning/datasets/__init__.py b/examples/pytorch_lightning/datasets/__init__.py new file mode 100644 index 0000000..5ac794b --- /dev/null +++ b/examples/pytorch_lightning/datasets/__init__.py @@ -0,0 +1,49 @@ +""" +This submodule contains data preparation code for some of the datasets used with our models, +i.e. MNIST, CIFAR 10 and 100 and ImageNet. +""" + +from typing import List, Type + +from .base import BasicDataset +from .cifar import CIFAR10, CIFAR100 +from .imagenet import ImageNet +from .mnist import MNIST + +__all__ = [ + "BasicDataset", + "dataset_from_name", + "dataset_names", + "MNIST", + "CIFAR10", + "CIFAR100", + "ImageNet", +] + + +def dataset_from_name(name: str) -> Type[BasicDataset]: + """returns the dataset to which the name belongs to (name has to be the value of the datasets + name-attribute) + + Args: + name (str): name of the dataset + + Raises: + ValueError: raised if no dataset under that name was found + + Returns: + dataset: the dataset + """ + for dataset_class in [CIFAR10, CIFAR100, ImageNet, MNIST]: + if dataset_class.name == name: + return dataset_class + raise Exception(f"unknown dataset: {name}") + + +def dataset_names() -> List[str]: + """getter for list of dataset names for argparse + + Returns: + List: the dataset names + """ + return [dataset_class.name for dataset_class in [CIFAR10, CIFAR100, ImageNet, MNIST]] diff --git a/examples/pytorch_lightning/datasets/base.py b/examples/pytorch_lightning/datasets/base.py new file mode 100644 index 0000000..c48bf18 --- /dev/null +++ b/examples/pytorch_lightning/datasets/base.py @@ -0,0 +1,137 @@ +import logging +import os +from pathlib import Path +from typing import Optional, Tuple, Any + +import torch +from torch.utils.data import Dataset +from torchvision.transforms import transforms + +from ..datasets.dummy_dataset import DummyDataset + + +class BasicDataset(Dataset): + name = "None" + num_classes = 0 + shape = (0, 0, 0, 0) + mean: Any = None + std_dev: Any = None + num_train_samples = 0 + num_val_samples = 0 + + def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + """initializes the dataset. + + Args: + train (bool): whether the train or test dataset is wanted + root_directory (str): path to main dataset storage directory + download (bool): whether train/test should be downloaded if it does not exist + + Returns: + Dataset: the created test/train dataset + """ + super(BasicDataset, self).__init__() + self.is_train = train + self._download = download + self.root_directory = self.get_dataset_root_directory(root_directory) + self.dataset = self.get_dataset(download) + + @classmethod + def get_train_and_test(cls, root_directory: str, download: bool = False) -> Tuple["BasicDataset", "BasicDataset"]: + """creates a pair of train and test dataset. + + Returns: + Tuple: the train and test dataset + """ + return cls(True, root_directory, download), cls(False, root_directory, download) + + @classmethod + def get_dummy_train_and_test_datasets(cls) -> Tuple[DummyDataset, DummyDataset]: + train_set = DummyDataset(cls.shape, cls.num_classes, cls.num_train_samples) # type: ignore + val_set = DummyDataset(cls.shape, cls.num_classes, cls.num_val_samples) # type: ignore + return train_set, val_set + + def get_dataset_root_directory(self, root_directory_argument: Optional[str]) -> Path: + """chooses the dataset root directory based on the passed argument or environment variables. + + Returns: + Tuple: the train and test dataset + """ + if root_directory_argument is not None: + return Path(root_directory_argument) + + environment_variable_name = f"{self.name.upper()}_HOME" + if os.environ.get(environment_variable_name) is not None: + return Path(os.environ.get(environment_variable_name)) # type: ignore + if os.environ.get("BITORCH_DATA_HOME") is not None: + return Path(os.environ.get("BITORCH_DATA_HOME")) / self.name # type: ignore + + environment_variable_hint = ( + f" To change this, set '{environment_variable_name}' or 'BITORCH_DATA_HOME' " + f"(in the latter case, the data resides in the folder '{self.name}' in BITORCH_DATA_HOME)." + f" Some datasets can be downloaded by adding the --download command line argument." + ) + if self._download: + logging.warning("Dataset is being downloaded to the directory './data'." + environment_variable_hint) + return Path("./data") + else: + raise ValueError(f"Dataset {self.name} not found." + environment_variable_hint) + + def get_dataset(self, download: bool) -> Dataset: + """creates the actual dataset + + Args: + download (bool): toggles if train/test shall be downloaded if possible + + Raises: + NotImplementedError: thrown, because this method needs to be overwritten by subclasses + + Returns: + Dataset: the created test/train dataset + """ + raise NotImplementedError() + + def get_transform(self) -> Any: + if self.is_train: + return self.train_transform() + return self.test_transform() + + @classmethod + def test_transform(cls) -> Any: + """get the transform for the test data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def train_transform(cls) -> Any: + """get the transform for the training data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def get_normalize_transform(cls) -> transforms.Normalize: + return transforms.Normalize(cls.mean, cls.std_dev) + + def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]: # type: ignore + """returns the item at the given index of the dataset. + + Args: + index (int): requested index + + Returns: + Tuple[torch.Tensor, torch.Tensor]: data and label at the specified index + """ + return self.dataset[index] + + def __len__(self) -> int: + return len(self.dataset) # type: ignore + + def num_samples(self) -> int: + """returns the (theoretical) dataset size.""" + return self.num_train_samples if self.is_train else self.num_val_samples diff --git a/examples/pytorch_lightning/datasets/cifar.py b/examples/pytorch_lightning/datasets/cifar.py new file mode 100644 index 0000000..40838c4 --- /dev/null +++ b/examples/pytorch_lightning/datasets/cifar.py @@ -0,0 +1,65 @@ +from abc import ABC + +from torch.utils.data import Dataset +from torchvision.datasets import cifar +from torchvision.transforms import transforms + +from .base import BasicDataset + +__all__ = ["CIFAR10", "CIFAR100"] + + +class CIFAR(BasicDataset, ABC): + shape = (1, 3, 32, 32) + num_train_samples = 50000 + num_val_samples = 10000 + + @classmethod + def train_transform(cls) -> transforms.Compose: + return transforms.Compose( + [ + transforms.RandomCrop(32, padding=4), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) + + @classmethod + def test_transform(cls) -> transforms.Compose: + return transforms.Compose( + [ + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) + + +class CIFAR10(CIFAR): + name = "cifar10" + num_classes = 10 + mean = (0.4914, 0.4822, 0.4465) + std_dev = (0.2023, 0.1994, 0.2010) + + def get_dataset(self, download: bool = True) -> Dataset: + return cifar.CIFAR10( + root=self.root_directory, + train=self.is_train, + transform=self.get_transform(), + download=download, + ) + + +class CIFAR100(CIFAR): + name = "cifar100" + num_classes = 100 + mean = (0.507, 0.487, 0.441) + std_dev = (0.267, 0.256, 0.276) + + def get_dataset(self, download: bool = True) -> Dataset: + return cifar.CIFAR100( + root=self.root_directory, + train=self.is_train, + transform=self.get_transform(), + download=download, + ) diff --git a/examples/pytorch_lightning/datasets/dummy_dataset.py b/examples/pytorch_lightning/datasets/dummy_dataset.py new file mode 100644 index 0000000..91b5524 --- /dev/null +++ b/examples/pytorch_lightning/datasets/dummy_dataset.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +import torch +from typing import Tuple + + +class DummyDataset(Dataset): + """An iterator that produces repeated dummy data. + Args: + data_sample: a data sample that should be produced at each step. + batch_size: the batch size for storing. + sample_count: number of `data` samples in the dummy dataset. + """ + + def __init__(self, data_shape: torch.Size, num_classes: int, sample_count: int) -> None: + self._data_sample = torch.zeros(data_shape) + self._class_sample = torch.zeros((num_classes,), dtype=torch.int64) + self._sample_count = sample_count + + def __len__(self) -> int: + return self._sample_count + + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: + return self._data_sample, self._class_sample diff --git a/examples/pytorch_lightning/datasets/imagenet.py b/examples/pytorch_lightning/datasets/imagenet.py new file mode 100644 index 0000000..d5b116d --- /dev/null +++ b/examples/pytorch_lightning/datasets/imagenet.py @@ -0,0 +1,52 @@ +from pathlib import Path + +from torch.utils.data import Dataset +from torchvision import transforms +from torchvision.datasets import ImageFolder + +from .base import BasicDataset + + +class ImageNet(BasicDataset): + name = "imagenet" + num_classes = 1000 + shape = (1, 3, 224, 224) + mean = (0.485, 0.456, 0.406) + std_dev = (0.229, 0.224, 0.255) + num_train_samples = 1281167 + num_val_samples = 50000 + + def get_data_dir(self) -> Path: + split = "train" if self.is_train else "val" + directory = self.root_directory / split + return directory + + def get_dataset(self, download: bool) -> Dataset: + directory = self.get_data_dir() + print("got directory for imagenet:", directory) + if download and not directory.is_dir(): + raise RuntimeError("ImageNet dataset must be downloaded and prepared manually.") + return ImageFolder(directory, transform=self.get_transform()) + + @classmethod + def train_transform(cls) -> transforms.Compose: + crop_scale = 0.08 + return transforms.Compose( + [ + transforms.RandomResizedCrop(224, scale=(crop_scale, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) + + @classmethod + def test_transform(cls) -> transforms.Compose: + return transforms.Compose( + [ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + cls.get_normalize_transform(), + ] + ) diff --git a/examples/pytorch_lightning/datasets/mnist.py b/examples/pytorch_lightning/datasets/mnist.py new file mode 100644 index 0000000..fde7f8e --- /dev/null +++ b/examples/pytorch_lightning/datasets/mnist.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +from torchvision.datasets import mnist + +from .base import BasicDataset + + +class MNIST(BasicDataset): + name = "mnist" + num_classes = 10 + shape = (1, 1, 28, 28) + + mean = (0.1307,) + std_dev = (0.3081,) + num_train_samples = 60000 + num_val_samples = 10000 + + def get_dataset(self, download: bool = True) -> Dataset: + return mnist.MNIST( + root=self.root_directory, + train=self.is_train, + transform=self.get_transform(), + download=download, + ) From 041fd781bfd23faa5fcc4e79ed2f26e286c38daa Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 14:38:16 +0200 Subject: [PATCH 119/208] removed unused imports --- tests/models/test_model_conversion.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 05b41de..55df0b6 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -17,10 +17,8 @@ from bitorch.layers.register import ( q_linear_registry, QLinearImplementation, - q_conv1d_registry, q_conv2d_registry, QConv2dImplementation, - q_conv3d_registry, ) from bitorch.models import Model From d0c239f050d6797139c3727e620b14b8e3298372 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 14:43:20 +0200 Subject: [PATCH 120/208] fixed linter --- examples/pytorch_lightning/utils/arg_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index bc971cf..21e7f8e 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -3,7 +3,7 @@ from typing import Tuple from bitorch.models import model_from_name, model_names -from ..datasets import dataset_names +from ..datasets import dataset_names # type: ignore from bitorch import add_config_args from pytorch_lightning import Trainer From 2b510d10bd41ce8fab9c50e95f72e90f48c7dcb2 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 15:03:36 +0200 Subject: [PATCH 121/208] fixed refactoring bugs --- bitorch/models/dlrm.py | 5 +- examples/dlrm/criteo.py | 76 ------------------------------- examples/dlrm/train_dlrm.py | 16 +++---- examples/dlrm/utils/arg_parser.py | 7 ++- 4 files changed, 12 insertions(+), 92 deletions(-) delete mode 100644 examples/dlrm/criteo.py diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index c12f426..cb6dd95 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -5,7 +5,6 @@ import torch from torch.nn import Linear, Sequential, PReLU, Sigmoid, EmbeddingBag, ModuleList, BatchNorm1d, Module import numpy as np -from bitorch.datasets.base import BasicDataset from bitorch.layers import QLinear from bitorch.models.base import Model @@ -108,7 +107,7 @@ class DLRM(Model): def __init__( self, - dataset: BasicDataset, + input_shape: List[int], dense_feature_size: int, embedding_dimension: int, embedding_layer_sizes: List[int], @@ -120,7 +119,7 @@ def __init__( binary_embedding: bool, **kwargs: Any, ) -> None: - super().__init__(dataset) + super().__init__(input_shape) self.interaction_operation = interaction_operation self.embedding_layers = create_embeddings( embedding_dimension, diff --git a/examples/dlrm/criteo.py b/examples/dlrm/criteo.py deleted file mode 100644 index 4b28123..0000000 --- a/examples/dlrm/criteo.py +++ /dev/null @@ -1,76 +0,0 @@ -import gc -from typing import Tuple -from torch.utils.data import Dataset -import logging -import torch -import os -import numpy as np -from bitorch.datasets.base import BasicDataset -from facebook_dataloading.dataloading_fb import CriteoDataset - - -class SplitCriteoDataset(Dataset): - """Dataset to get items from a dataset for each split. Useful if dataset creation takes a lot of time and can be done exactly once.""" - - def __init__( - self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0 - ) -> None: - self.dataset = dataset - self.indices = self.dataset.train_indices if split == "train" else self.dataset.test_indices - - dataset_size = int(len(self.indices) * (1.0 - ignore_size)) - self.indices = np.random.choice(self.indices, size=dataset_size, replace=False) - - def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: - return self.dataset[self.indices[idx]] - - def __len__(self) -> int: - return len(self.indices) - - -class Criteo(BasicDataset): - name = "criteo" - - num_train_samples = 60000 - num_val_samples = 10000 - dataset_url = "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz" - - def get_dataset(self, download: bool = True) -> Dataset: - try: - self.download_path = self.root_directory / "criteo.tar.gz" - self.path = self.root_directory / "train.txt" - self.path.parent.mkdir(parents=True, exist_ok=True) - if download and not self.download_path.exists(): - logging.info("DOWNLOADING CRITEO DATASET") - result = os.system(f"wget {self.dataset_url} -O {str(self.root_directory / 'criteo.tar.gz')}") - if result != 0: - raise Exception("Download failed") - logging.info("FINISHED DOWNLOAD") - if not (self.root_directory / "train.txt").exists(): - logging.info("EXTRACTING CRITEO DATASET") - result = os.system(f"tar -xf {str(self.root_directory / 'criteo.tar.gz')} -C {self.root_directory}") - if result != 0: - raise Exception("Extraction failed") - logging.info("FINISHED EXTRACTION") - except Exception as e: - logging.error( - f"Cannot get criteo dataset: {e}. You need download " - "the dataset manually under the following link: " - "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz " - f"and extract it to the following path: {str(self.root_directory.resolve())}. " - "alternatively you can try downloading it automatically by using the --download flag" - ) - dataset = CriteoDataset( - dataset="kaggle", - max_ind_range=-1, - sub_sample_rate=0.0, - randomize="total", - # split="train" if self.is_train else "test", - raw_path=str(self.root_directory / "train.txt"), - pro_data=str(self.root_directory / "preprocessed.npz"), - memory_map=False, - dataset_multiprocessing=True, - store_all_indices=True, - ) - gc.collect() - return dataset diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index 969e518..30ee675 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -23,7 +23,6 @@ from torch.utils.data import DataLoader from bitorch import apply_args_to_configuration -from bitorch.datasets import dataset_from_name from bitorch.models import DLRM from bitorch.quantizations import Quantization @@ -32,9 +31,8 @@ from utils.utils import configure_logging from utils.log import CommandLineLogger -from criteo import Criteo, SplitCriteoDataset +from datasets.criteo import Criteo, SplitCriteoDataset from facebook_dataloading.dataloading_fb import collate_wrapper_criteo_offset -from bitorch.datasets import datasets_by_name logger = logging.getLogger() @@ -56,7 +54,7 @@ def make_dlrm_dataloaders( Tuple[Dataloader, Dataloader, int, int]: the dataloaders, the size of the dense features and the size of embedding layers """ logging.info("loading Criteo dataset...") - dataset = Criteo(True, root_directory=dataset_dir, download=download, augmentation=None).dataset + dataset = Criteo(True, root_directory=dataset_dir, download=download).dataset train_dataset = SplitCriteoDataset(dataset, "train", ignore_size=ignore_size) test_dataset = SplitCriteoDataset(dataset, "test", ignore_size=ignore_size) @@ -133,8 +131,6 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: lr_monitor = LearningRateMonitor(logging_interval="step") callbacks.append(lr_monitor) - datasets_by_name["criteo"] = Criteo - dataset = dataset_from_name(args.dataset) if args.dataset == "criteo": train_loader, test_loader, dense_feature_size, embedding_layer_sizes = make_dlrm_dataloaders( args.dataset_dir, @@ -151,7 +147,11 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - model = DLRM(**model_kwargs, embedding_layer_sizes=embedding_layer_sizes, dataset=dataset, dense_feature_size=dense_feature_size) # type: ignore + data_point = iter(train_loader).next() + data_point = (data_point[0], (data_point[1], data_point[2])) + print("DATA SHAPE:", (data_point[0].shape, (data_point[1].shape, data_point[2].shape))) + + model = DLRM(**model_kwargs, embedding_layer_sizes=embedding_layer_sizes, input_shape=[], dense_feature_size=dense_feature_size) # type: ignore model.initialize() if args.checkpoint_load is not None and args.pretrained: logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") @@ -173,8 +173,6 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") logger.info(f"max_epochs: {args.max_epochs}") - data_point = iter(train_loader).next() - data_point = (data_point[0], (data_point[1], data_point[2])) computational_intensity = fv_nn.FlopCountAnalysis(model, inputs=data_point, quantization_base_class=Quantization) stats, table = fv_nn.flop_count_table(computational_intensity, automatic_qmodules=True) diff --git a/examples/dlrm/utils/arg_parser.py b/examples/dlrm/utils/arg_parser.py index 64d7e4f..c5d64f1 100644 --- a/examples/dlrm/utils/arg_parser.py +++ b/examples/dlrm/utils/arg_parser.py @@ -3,9 +3,8 @@ from typing import Tuple from bitorch.models import model_from_name, model_names -from bitorch.datasets import dataset_names from bitorch import add_config_args -from ....bitorch.models.dlrm import DLRM +from bitorch.models.dlrm import DLRM from pytorch_lightning import Trainer @@ -168,8 +167,8 @@ def add_dataset_args(parser: ArgumentParser) -> None: data.add_argument( "--dataset", type=str, - default="cifar10", - choices=dataset_names() + ["criteo"], + default="criteo", + choices=["criteo"], help="name of the dataset to be used for training", ) data.add_argument( From 2f13067857b528d2fa11efddeea548bc09bcf3ab Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 15:05:22 +0200 Subject: [PATCH 122/208] added dlrm datasett files --- examples/dlrm/datasets/__init__.py | 42 ++++++++ examples/dlrm/datasets/base.py | 137 ++++++++++++++++++++++++ examples/dlrm/datasets/criteo.py | 76 +++++++++++++ examples/dlrm/datasets/dummy_dataset.py | 23 ++++ 4 files changed, 278 insertions(+) create mode 100644 examples/dlrm/datasets/__init__.py create mode 100644 examples/dlrm/datasets/base.py create mode 100644 examples/dlrm/datasets/criteo.py create mode 100644 examples/dlrm/datasets/dummy_dataset.py diff --git a/examples/dlrm/datasets/__init__.py b/examples/dlrm/datasets/__init__.py new file mode 100644 index 0000000..44adf35 --- /dev/null +++ b/examples/dlrm/datasets/__init__.py @@ -0,0 +1,42 @@ +""" +This submodule contains data preparation code for some of the datasets used with our models, +i.e. MNIST, CIFAR 10 and 100 and ImageNet. +""" + +from typing import List, Type + +from .base import BasicDataset +from .criteo import Criteo + +__all__ = [ + "BasicDataset", + "Criteo", +] + + +def dataset_from_name(name: str) -> Type[BasicDataset]: + """returns the dataset to which the name belongs to (name has to be the value of the datasets + name-attribute) + + Args: + name (str): name of the dataset + + Raises: + ValueError: raised if no dataset under that name was found + + Returns: + dataset: the dataset + """ + for dataset_class in [Criteo]: + if dataset_class.name == name: + return dataset_class + raise Exception(f"unknown dataset: {name}") + + +def dataset_names() -> List[str]: + """getter for list of dataset names for argparse + + Returns: + List: the dataset names + """ + return [dataset_class.name for dataset_class in [Criteo]] diff --git a/examples/dlrm/datasets/base.py b/examples/dlrm/datasets/base.py new file mode 100644 index 0000000..f006b59 --- /dev/null +++ b/examples/dlrm/datasets/base.py @@ -0,0 +1,137 @@ +import logging +import os +from pathlib import Path +from typing import Optional, Tuple, Any + +import torch +from torch.utils.data import Dataset +from torchvision.transforms import transforms + +from .dummy_dataset import DummyDataset + + +class BasicDataset(Dataset): + name = "None" + num_classes = 0 + shape = (0, 0, 0, 0) + mean: Any = None + std_dev: Any = None + num_train_samples = 0 + num_val_samples = 0 + + def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + """initializes the dataset. + + Args: + train (bool): whether the train or test dataset is wanted + root_directory (str): path to main dataset storage directory + download (bool): whether train/test should be downloaded if it does not exist + + Returns: + Dataset: the created test/train dataset + """ + super(BasicDataset, self).__init__() + self.is_train = train + self._download = download + self.root_directory = self.get_dataset_root_directory(root_directory) + self.dataset = self.get_dataset(download) + + @classmethod + def get_train_and_test(cls, root_directory: str, download: bool = False) -> Tuple["BasicDataset", "BasicDataset"]: + """creates a pair of train and test dataset. + + Returns: + Tuple: the train and test dataset + """ + return cls(True, root_directory, download), cls(False, root_directory, download) + + @classmethod + def get_dummy_train_and_test_datasets(cls) -> Tuple[DummyDataset, DummyDataset]: + train_set = DummyDataset(cls.shape, cls.num_classes, cls.num_train_samples) # type: ignore + val_set = DummyDataset(cls.shape, cls.num_classes, cls.num_val_samples) # type: ignore + return train_set, val_set + + def get_dataset_root_directory(self, root_directory_argument: Optional[str]) -> Path: + """chooses the dataset root directory based on the passed argument or environment variables. + + Returns: + Tuple: the train and test dataset + """ + if root_directory_argument is not None: + return Path(root_directory_argument) + + environment_variable_name = f"{self.name.upper()}_HOME" + if os.environ.get(environment_variable_name) is not None: + return Path(os.environ.get(environment_variable_name)) # type: ignore + if os.environ.get("BITORCH_DATA_HOME") is not None: + return Path(os.environ.get("BITORCH_DATA_HOME")) / self.name # type: ignore + + environment_variable_hint = ( + f" To change this, set '{environment_variable_name}' or 'BITORCH_DATA_HOME' " + f"(in the latter case, the data resides in the folder '{self.name}' in BITORCH_DATA_HOME)." + f" Some datasets can be downloaded by adding the --download command line argument." + ) + if self._download: + logging.warning("Dataset is being downloaded to the directory './data'." + environment_variable_hint) + return Path("./data") + else: + raise ValueError(f"Dataset {self.name} not found." + environment_variable_hint) + + def get_dataset(self, download: bool) -> Dataset: + """creates the actual dataset + + Args: + download (bool): toggles if train/test shall be downloaded if possible + + Raises: + NotImplementedError: thrown, because this method needs to be overwritten by subclasses + + Returns: + Dataset: the created test/train dataset + """ + raise NotImplementedError() + + def get_transform(self) -> Any: + if self.is_train: + return self.train_transform() + return self.test_transform() + + @classmethod + def test_transform(cls) -> Any: + """get the transform for the test data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def train_transform(cls) -> Any: + """get the transform for the training data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def get_normalize_transform(cls) -> transforms.Normalize: + return transforms.Normalize(cls.mean, cls.std_dev) + + def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]: # type: ignore + """returns the item at the given index of the dataset. + + Args: + index (int): requested index + + Returns: + Tuple[torch.Tensor, torch.Tensor]: data and label at the specified index + """ + return self.dataset[index] + + def __len__(self) -> int: + return len(self.dataset) # type: ignore + + def num_samples(self) -> int: + """returns the (theoretical) dataset size.""" + return self.num_train_samples if self.is_train else self.num_val_samples diff --git a/examples/dlrm/datasets/criteo.py b/examples/dlrm/datasets/criteo.py new file mode 100644 index 0000000..d700348 --- /dev/null +++ b/examples/dlrm/datasets/criteo.py @@ -0,0 +1,76 @@ +import gc +from typing import Tuple +from torch.utils.data import Dataset +import logging +import torch +import os +import numpy as np +from .base import BasicDataset +from facebook_dataloading.dataloading_fb import CriteoDataset + + +class SplitCriteoDataset(Dataset): + """Dataset to get items from a dataset for each split. Useful if dataset creation takes a lot of time and can be done exactly once.""" + + def __init__( + self, dataset: BasicDataset, split: str, train_split_fraction: float = 0.9, ignore_size: float = 0.0 + ) -> None: + self.dataset = dataset + self.indices = self.dataset.train_indices if split == "train" else self.dataset.test_indices + + dataset_size = int(len(self.indices) * (1.0 - ignore_size)) + self.indices = np.random.choice(self.indices, size=dataset_size, replace=False) + + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: + return self.dataset[self.indices[idx]] + + def __len__(self) -> int: + return len(self.indices) + + +class Criteo(BasicDataset): + name = "criteo" + + num_train_samples = 60000 + num_val_samples = 10000 + dataset_url = "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz" + + def get_dataset(self, download: bool = True) -> Dataset: + try: + self.download_path = self.root_directory / "criteo.tar.gz" + self.path = self.root_directory / "train.txt" + self.path.parent.mkdir(parents=True, exist_ok=True) + if download and not self.download_path.exists(): + logging.info("DOWNLOADING CRITEO DATASET") + result = os.system(f"wget {self.dataset_url} -O {str(self.root_directory / 'criteo.tar.gz')}") + if result != 0: + raise Exception("Download failed") + logging.info("FINISHED DOWNLOAD") + if not (self.root_directory / "train.txt").exists(): + logging.info("EXTRACTING CRITEO DATASET") + result = os.system(f"tar -xf {str(self.root_directory / 'criteo.tar.gz')} -C {self.root_directory}") + if result != 0: + raise Exception("Extraction failed") + logging.info("FINISHED EXTRACTION") + except Exception as e: + logging.error( + f"Cannot get criteo dataset: {e}. You need download " + "the dataset manually under the following link: " + "http://go.criteo.net/criteo-research-kaggle-display-advertising-challenge-dataset.tar.gz " + f"and extract it to the following path: {str(self.root_directory.resolve())}. " + "alternatively you can try downloading it automatically by using the --download flag" + ) + dataset = CriteoDataset( + dataset="kaggle", + max_ind_range=-1, + sub_sample_rate=0.0, + randomize="total", + # split="train" if self.is_train else "test", + raw_path=str(self.root_directory / "train.txt"), + pro_data=str(self.root_directory / "preprocessed.npz"), + memory_map=False, + dataset_multiprocessing=True, + store_all_indices=True, + ) + gc.collect() + return dataset diff --git a/examples/dlrm/datasets/dummy_dataset.py b/examples/dlrm/datasets/dummy_dataset.py new file mode 100644 index 0000000..91b5524 --- /dev/null +++ b/examples/dlrm/datasets/dummy_dataset.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +import torch +from typing import Tuple + + +class DummyDataset(Dataset): + """An iterator that produces repeated dummy data. + Args: + data_sample: a data sample that should be produced at each step. + batch_size: the batch size for storing. + sample_count: number of `data` samples in the dummy dataset. + """ + + def __init__(self, data_shape: torch.Size, num_classes: int, sample_count: int) -> None: + self._data_sample = torch.zeros(data_shape) + self._class_sample = torch.zeros((num_classes,), dtype=torch.int64) + self._sample_count = sample_count + + def __len__(self) -> int: + return self._sample_count + + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: + return self._data_sample, self._class_sample From 60ef7da6a09395e5ee403fa6980d515e710c7a0d Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 15:59:52 +0200 Subject: [PATCH 123/208] added dlrm model test --- bitorch/models/dlrm.py | 16 ++++++++-------- examples/dlrm/train_dlrm.py | 5 +++-- examples/mnist/train_mnist.py | 5 +++-- tests/models/test_models.py | 20 ++++++++++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index cb6dd95..401fb62 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -107,16 +107,16 @@ class DLRM(Model): def __init__( self, - input_shape: List[int], dense_feature_size: int, - embedding_dimension: int, embedding_layer_sizes: List[int], - bottom_mlp_layer_sizes: Union[List[int], str], - top_mlp_layer_sizes: Union[List[int], str], - interaction_operation: Interaction_Operation_Type, - binary_bottom_mlp: bool, - binary_top_mlp: bool, - binary_embedding: bool, + input_shape: List[int] = [], + bottom_mlp_layer_sizes: Union[List[int], str] = [512, 256, 64], + top_mlp_layer_sizes: Union[List[int], str] = [512, 256, 1], + interaction_operation: Interaction_Operation_Type = Interaction_Operation_Type.PRODUCT, + binary_bottom_mlp: bool = False, + binary_top_mlp: bool = True, + binary_embedding: bool = True, + embedding_dimension: int = 16, **kwargs: Any, ) -> None: super().__init__(input_shape) diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index 30ee675..bbf4c9b 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -148,8 +148,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger.debug(f"got model args as dict: {model_kwargs}") data_point = iter(train_loader).next() - data_point = (data_point[0], (data_point[1], data_point[2])) + print("DATA SHAPE:", (type(data_point[0]), (type(data_point[1]), type(data_point[2])))) print("DATA SHAPE:", (data_point[0].shape, (data_point[1].shape, data_point[2].shape))) + data_point = (data_point[0], (data_point[1], data_point[2])) model = DLRM(**model_kwargs, embedding_layer_sizes=embedding_layer_sizes, input_shape=[], dense_feature_size=dense_feature_size) # type: ignore model.initialize() @@ -169,7 +170,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: callbacks=callbacks, # type: ignore log_every_n_steps=args.log_interval, ) - logger.info(f"model: {args.model}") + logger.info("model: DLRM") logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") logger.info(f"max_epochs: {args.max_epochs}") diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 0d7ec46..4f5a18c 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -14,7 +14,8 @@ from torch.optim.lr_scheduler import StepLR import bitorch.layers as qnn -from bitorch import datasets as bitorch_datasets, RuntimeMode +from bitorch import RuntimeMode +from datasets import MNIST from bitorch.layers import convert import bitorch_inference_engine @@ -156,7 +157,7 @@ def main(): train_kwargs.update(cuda_kwargs) test_kwargs.update(cuda_kwargs) - train_dataset, test_dataset = bitorch_datasets.MNIST.get_train_and_test(download=True) + train_dataset, test_dataset = MNIST.get_train_and_test(download=True) train_loader = torch.utils.data.DataLoader(train_dataset, **train_kwargs) test_loader = torch.utils.data.DataLoader(test_dataset, **test_kwargs) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 73e176b..ebf23e5 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -11,6 +11,7 @@ ResnetE, ResnetE18, ResnetE34, + DLRM, ) import torch import numpy as np @@ -23,6 +24,8 @@ CIFAR100 = [(1, 3, 32, 32), 100, "CIFAR100"] IMAGENET = [(1, 3, 224, 224), 1000, "IMAGENET"] +CRITEO = [([1, 13], ([26, 1], [26, 1])), 1, "CRITEO"] + ALL_DATASETS = [MNIST, CIFAR10, CIFAR100, IMAGENET] RGB_DATASETS = [CIFAR10, CIFAR100, IMAGENET] @@ -42,6 +45,7 @@ [ResnetE18, {}, RGB_DATASETS], [ResnetE34, {}, RGB_DATASETS], [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], + [DLRM, {}, [CRITEO]], ] @@ -61,12 +65,16 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: if dataset is IMAGENET: batch_sizes_to_test = [1, 2] for batch_size in batch_sizes_to_test: - input_shape = list(dataset[0]) - input_shape[0] = batch_size - - model = model_class(input_shape=dataset[0], num_classes=dataset[1], **combination) - input_values = torch.Tensor(np.random.uniform(0, 1.0, input_shape)) - output = model(input_values) + if model_class.name == "dlrm": + model = model_class(dense_feature_size=dataset[0][0][1], embedding_layer_sizes=[100] * dataset[0][1][0][0], **combination) + input_values = (torch.Tensor(np.random.uniform(0, 1.0, dataset[0])), (torch.zeros(dataset[1][0]), torch.zeros(dataset[1][1]))) + output = model(*input_values) + else: + input_shape = list(dataset[0]) + input_shape[0] = batch_size + model = model_class(input_shape=dataset[0], num_classes=dataset[1], **combination) + input_values = torch.Tensor(np.random.uniform(0, 1.0, input_shape)) + output = model(input_values) assert torch.equal( torch.as_tensor(output.shape), torch.Tensor([input_shape[0], dataset[1]]).long(), From 39a4a9a7a846b6e83ce827c50ac1460e067710b2 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:08:11 +0200 Subject: [PATCH 124/208] fixed dlrm test --- bitorch/models/dlrm.py | 2 +- tests/models/test_models.py | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 401fb62..75adbf7 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -112,7 +112,7 @@ def __init__( input_shape: List[int] = [], bottom_mlp_layer_sizes: Union[List[int], str] = [512, 256, 64], top_mlp_layer_sizes: Union[List[int], str] = [512, 256, 1], - interaction_operation: Interaction_Operation_Type = Interaction_Operation_Type.PRODUCT, + interaction_operation: Interaction_Operation_Type = Interaction_Operation_Type.PRODUCT.value, binary_bottom_mlp: bool = False, binary_top_mlp: bool = True, binary_embedding: bool = True, diff --git a/tests/models/test_models.py b/tests/models/test_models.py index ebf23e5..645d778 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -26,21 +26,21 @@ CRITEO = [([1, 13], ([26, 1], [26, 1])), 1, "CRITEO"] -ALL_DATASETS = [MNIST, CIFAR10, CIFAR100, IMAGENET] +ALL_IMAGE_DATASETS = [MNIST, CIFAR10, CIFAR100, IMAGENET] RGB_DATASETS = [CIFAR10, CIFAR100, IMAGENET] TEST_INPUT_DATA = [ [ Resnet, {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, - ALL_DATASETS, + ALL_IMAGE_DATASETS, ], - [Resnet18V1, {}, ALL_DATASETS], - [Resnet34V1, {}, ALL_DATASETS], - [Resnet50V1, {}, ALL_DATASETS], - [Resnet18V2, {}, ALL_DATASETS], - [Resnet34V2, {}, ALL_DATASETS], - [Resnet50V2, {}, ALL_DATASETS], + [Resnet18V1, {}, ALL_IMAGE_DATASETS], + [Resnet34V1, {}, ALL_IMAGE_DATASETS], + [Resnet50V1, {}, ALL_IMAGE_DATASETS], + [Resnet18V2, {}, ALL_IMAGE_DATASETS], + [Resnet34V2, {}, ALL_IMAGE_DATASETS], + [Resnet50V2, {}, ALL_IMAGE_DATASETS], [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], [ResnetE18, {}, RGB_DATASETS], [ResnetE34, {}, RGB_DATASETS], @@ -50,7 +50,7 @@ @pytest.mark.parametrize("model_class, model_kwargs, datasets_to_test", TEST_INPUT_DATA) -@pytest.mark.parametrize("dataset", ALL_DATASETS) +@pytest.mark.parametrize("dataset", [MNIST, CIFAR10, CIFAR100, IMAGENET, CRITEO]) def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: assert models_by_name[model_class.name] is model_class if dataset not in datasets_to_test: @@ -67,7 +67,10 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: for batch_size in batch_sizes_to_test: if model_class.name == "dlrm": model = model_class(dense_feature_size=dataset[0][0][1], embedding_layer_sizes=[100] * dataset[0][1][0][0], **combination) - input_values = (torch.Tensor(np.random.uniform(0, 1.0, dataset[0])), (torch.zeros(dataset[1][0]), torch.zeros(dataset[1][1]))) + dataset[0][0][0] = batch_size + dataset[0][1][0][1] = batch_size + dataset[0][1][1][1] = batch_size + input_values = (torch.Tensor(np.random.uniform(0, 1.0, dataset[0][0])), (torch.zeros(dataset[0][1][0], dtype=int), torch.zeros(dataset[0][1][1], dtype=int))) output = model(*input_values) else: input_shape = list(dataset[0]) @@ -77,5 +80,5 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: output = model(input_values) assert torch.equal( torch.as_tensor(output.shape), - torch.Tensor([input_shape[0], dataset[1]]).long(), + torch.Tensor([batch_size, dataset[1]]).long(), ) From b2b29fec91ab5b17d9370af9e0823708aaaac52e Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:08:32 +0200 Subject: [PATCH 125/208] fixed refactoring bug --- examples/mnist/datasets/__init__.py | 49 ++++++++++ examples/mnist/datasets/base.py | 137 ++++++++++++++++++++++++++++ examples/mnist/datasets/mnist.py | 23 +++++ 3 files changed, 209 insertions(+) create mode 100644 examples/mnist/datasets/__init__.py create mode 100644 examples/mnist/datasets/base.py create mode 100644 examples/mnist/datasets/mnist.py diff --git a/examples/mnist/datasets/__init__.py b/examples/mnist/datasets/__init__.py new file mode 100644 index 0000000..5ac794b --- /dev/null +++ b/examples/mnist/datasets/__init__.py @@ -0,0 +1,49 @@ +""" +This submodule contains data preparation code for some of the datasets used with our models, +i.e. MNIST, CIFAR 10 and 100 and ImageNet. +""" + +from typing import List, Type + +from .base import BasicDataset +from .cifar import CIFAR10, CIFAR100 +from .imagenet import ImageNet +from .mnist import MNIST + +__all__ = [ + "BasicDataset", + "dataset_from_name", + "dataset_names", + "MNIST", + "CIFAR10", + "CIFAR100", + "ImageNet", +] + + +def dataset_from_name(name: str) -> Type[BasicDataset]: + """returns the dataset to which the name belongs to (name has to be the value of the datasets + name-attribute) + + Args: + name (str): name of the dataset + + Raises: + ValueError: raised if no dataset under that name was found + + Returns: + dataset: the dataset + """ + for dataset_class in [CIFAR10, CIFAR100, ImageNet, MNIST]: + if dataset_class.name == name: + return dataset_class + raise Exception(f"unknown dataset: {name}") + + +def dataset_names() -> List[str]: + """getter for list of dataset names for argparse + + Returns: + List: the dataset names + """ + return [dataset_class.name for dataset_class in [CIFAR10, CIFAR100, ImageNet, MNIST]] diff --git a/examples/mnist/datasets/base.py b/examples/mnist/datasets/base.py new file mode 100644 index 0000000..c48bf18 --- /dev/null +++ b/examples/mnist/datasets/base.py @@ -0,0 +1,137 @@ +import logging +import os +from pathlib import Path +from typing import Optional, Tuple, Any + +import torch +from torch.utils.data import Dataset +from torchvision.transforms import transforms + +from ..datasets.dummy_dataset import DummyDataset + + +class BasicDataset(Dataset): + name = "None" + num_classes = 0 + shape = (0, 0, 0, 0) + mean: Any = None + std_dev: Any = None + num_train_samples = 0 + num_val_samples = 0 + + def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + """initializes the dataset. + + Args: + train (bool): whether the train or test dataset is wanted + root_directory (str): path to main dataset storage directory + download (bool): whether train/test should be downloaded if it does not exist + + Returns: + Dataset: the created test/train dataset + """ + super(BasicDataset, self).__init__() + self.is_train = train + self._download = download + self.root_directory = self.get_dataset_root_directory(root_directory) + self.dataset = self.get_dataset(download) + + @classmethod + def get_train_and_test(cls, root_directory: str, download: bool = False) -> Tuple["BasicDataset", "BasicDataset"]: + """creates a pair of train and test dataset. + + Returns: + Tuple: the train and test dataset + """ + return cls(True, root_directory, download), cls(False, root_directory, download) + + @classmethod + def get_dummy_train_and_test_datasets(cls) -> Tuple[DummyDataset, DummyDataset]: + train_set = DummyDataset(cls.shape, cls.num_classes, cls.num_train_samples) # type: ignore + val_set = DummyDataset(cls.shape, cls.num_classes, cls.num_val_samples) # type: ignore + return train_set, val_set + + def get_dataset_root_directory(self, root_directory_argument: Optional[str]) -> Path: + """chooses the dataset root directory based on the passed argument or environment variables. + + Returns: + Tuple: the train and test dataset + """ + if root_directory_argument is not None: + return Path(root_directory_argument) + + environment_variable_name = f"{self.name.upper()}_HOME" + if os.environ.get(environment_variable_name) is not None: + return Path(os.environ.get(environment_variable_name)) # type: ignore + if os.environ.get("BITORCH_DATA_HOME") is not None: + return Path(os.environ.get("BITORCH_DATA_HOME")) / self.name # type: ignore + + environment_variable_hint = ( + f" To change this, set '{environment_variable_name}' or 'BITORCH_DATA_HOME' " + f"(in the latter case, the data resides in the folder '{self.name}' in BITORCH_DATA_HOME)." + f" Some datasets can be downloaded by adding the --download command line argument." + ) + if self._download: + logging.warning("Dataset is being downloaded to the directory './data'." + environment_variable_hint) + return Path("./data") + else: + raise ValueError(f"Dataset {self.name} not found." + environment_variable_hint) + + def get_dataset(self, download: bool) -> Dataset: + """creates the actual dataset + + Args: + download (bool): toggles if train/test shall be downloaded if possible + + Raises: + NotImplementedError: thrown, because this method needs to be overwritten by subclasses + + Returns: + Dataset: the created test/train dataset + """ + raise NotImplementedError() + + def get_transform(self) -> Any: + if self.is_train: + return self.train_transform() + return self.test_transform() + + @classmethod + def test_transform(cls) -> Any: + """get the transform for the test data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def train_transform(cls) -> Any: + """get the transform for the training data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def get_normalize_transform(cls) -> transforms.Normalize: + return transforms.Normalize(cls.mean, cls.std_dev) + + def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]: # type: ignore + """returns the item at the given index of the dataset. + + Args: + index (int): requested index + + Returns: + Tuple[torch.Tensor, torch.Tensor]: data and label at the specified index + """ + return self.dataset[index] + + def __len__(self) -> int: + return len(self.dataset) # type: ignore + + def num_samples(self) -> int: + """returns the (theoretical) dataset size.""" + return self.num_train_samples if self.is_train else self.num_val_samples diff --git a/examples/mnist/datasets/mnist.py b/examples/mnist/datasets/mnist.py new file mode 100644 index 0000000..fde7f8e --- /dev/null +++ b/examples/mnist/datasets/mnist.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +from torchvision.datasets import mnist + +from .base import BasicDataset + + +class MNIST(BasicDataset): + name = "mnist" + num_classes = 10 + shape = (1, 1, 28, 28) + + mean = (0.1307,) + std_dev = (0.3081,) + num_train_samples = 60000 + num_val_samples = 10000 + + def get_dataset(self, download: bool = True) -> Dataset: + return mnist.MNIST( + root=self.root_directory, + train=self.is_train, + transform=self.get_transform(), + download=download, + ) From a0072f8305c4e196ebcc461c037dd20b29aa192d Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:15:09 +0200 Subject: [PATCH 126/208] fixed mnist example import bug --- examples/mnist/datasets/__init__.py | 44 ++++++++ examples/mnist/datasets/base.py | 137 +++++++++++++++++++++++ examples/mnist/datasets/dummy_dataset.py | 23 ++++ examples/mnist/datasets/mnist.py | 23 ++++ examples/mnist/train_mnist.py | 5 +- 5 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 examples/mnist/datasets/__init__.py create mode 100644 examples/mnist/datasets/base.py create mode 100644 examples/mnist/datasets/dummy_dataset.py create mode 100644 examples/mnist/datasets/mnist.py diff --git a/examples/mnist/datasets/__init__.py b/examples/mnist/datasets/__init__.py new file mode 100644 index 0000000..f334be7 --- /dev/null +++ b/examples/mnist/datasets/__init__.py @@ -0,0 +1,44 @@ +""" +This submodule contains data preparation code for some of the datasets used with our models, +i.e. MNIST, CIFAR 10 and 100 and ImageNet. +""" + +from typing import List, Type + +from .base import BasicDataset +from .mnist import MNIST + +__all__ = [ + "BasicDataset", + "dataset_from_name", + "dataset_names", + "MNIST", +] + + +def dataset_from_name(name: str) -> Type[BasicDataset]: + """returns the dataset to which the name belongs to (name has to be the value of the datasets + name-attribute) + + Args: + name (str): name of the dataset + + Raises: + ValueError: raised if no dataset under that name was found + + Returns: + dataset: the dataset + """ + for dataset_class in [MNIST]: + if dataset_class.name == name: + return dataset_class + raise Exception(f"unknown dataset: {name}") + + +def dataset_names() -> List[str]: + """getter for list of dataset names for argparse + + Returns: + List: the dataset names + """ + return [dataset_class.name for dataset_class in [MNIST]] diff --git a/examples/mnist/datasets/base.py b/examples/mnist/datasets/base.py new file mode 100644 index 0000000..c48bf18 --- /dev/null +++ b/examples/mnist/datasets/base.py @@ -0,0 +1,137 @@ +import logging +import os +from pathlib import Path +from typing import Optional, Tuple, Any + +import torch +from torch.utils.data import Dataset +from torchvision.transforms import transforms + +from ..datasets.dummy_dataset import DummyDataset + + +class BasicDataset(Dataset): + name = "None" + num_classes = 0 + shape = (0, 0, 0, 0) + mean: Any = None + std_dev: Any = None + num_train_samples = 0 + num_val_samples = 0 + + def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + """initializes the dataset. + + Args: + train (bool): whether the train or test dataset is wanted + root_directory (str): path to main dataset storage directory + download (bool): whether train/test should be downloaded if it does not exist + + Returns: + Dataset: the created test/train dataset + """ + super(BasicDataset, self).__init__() + self.is_train = train + self._download = download + self.root_directory = self.get_dataset_root_directory(root_directory) + self.dataset = self.get_dataset(download) + + @classmethod + def get_train_and_test(cls, root_directory: str, download: bool = False) -> Tuple["BasicDataset", "BasicDataset"]: + """creates a pair of train and test dataset. + + Returns: + Tuple: the train and test dataset + """ + return cls(True, root_directory, download), cls(False, root_directory, download) + + @classmethod + def get_dummy_train_and_test_datasets(cls) -> Tuple[DummyDataset, DummyDataset]: + train_set = DummyDataset(cls.shape, cls.num_classes, cls.num_train_samples) # type: ignore + val_set = DummyDataset(cls.shape, cls.num_classes, cls.num_val_samples) # type: ignore + return train_set, val_set + + def get_dataset_root_directory(self, root_directory_argument: Optional[str]) -> Path: + """chooses the dataset root directory based on the passed argument or environment variables. + + Returns: + Tuple: the train and test dataset + """ + if root_directory_argument is not None: + return Path(root_directory_argument) + + environment_variable_name = f"{self.name.upper()}_HOME" + if os.environ.get(environment_variable_name) is not None: + return Path(os.environ.get(environment_variable_name)) # type: ignore + if os.environ.get("BITORCH_DATA_HOME") is not None: + return Path(os.environ.get("BITORCH_DATA_HOME")) / self.name # type: ignore + + environment_variable_hint = ( + f" To change this, set '{environment_variable_name}' or 'BITORCH_DATA_HOME' " + f"(in the latter case, the data resides in the folder '{self.name}' in BITORCH_DATA_HOME)." + f" Some datasets can be downloaded by adding the --download command line argument." + ) + if self._download: + logging.warning("Dataset is being downloaded to the directory './data'." + environment_variable_hint) + return Path("./data") + else: + raise ValueError(f"Dataset {self.name} not found." + environment_variable_hint) + + def get_dataset(self, download: bool) -> Dataset: + """creates the actual dataset + + Args: + download (bool): toggles if train/test shall be downloaded if possible + + Raises: + NotImplementedError: thrown, because this method needs to be overwritten by subclasses + + Returns: + Dataset: the created test/train dataset + """ + raise NotImplementedError() + + def get_transform(self) -> Any: + if self.is_train: + return self.train_transform() + return self.test_transform() + + @classmethod + def test_transform(cls) -> Any: + """get the transform for the test data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def train_transform(cls) -> Any: + """get the transform for the training data. + + Returns: + transform: the transform pipeline + """ + return transforms.Compose([transforms.ToTensor(), cls.get_normalize_transform()]) + + @classmethod + def get_normalize_transform(cls) -> transforms.Normalize: + return transforms.Normalize(cls.mean, cls.std_dev) + + def __getitem__(self, index: int) -> Tuple[torch.Tensor, torch.Tensor]: # type: ignore + """returns the item at the given index of the dataset. + + Args: + index (int): requested index + + Returns: + Tuple[torch.Tensor, torch.Tensor]: data and label at the specified index + """ + return self.dataset[index] + + def __len__(self) -> int: + return len(self.dataset) # type: ignore + + def num_samples(self) -> int: + """returns the (theoretical) dataset size.""" + return self.num_train_samples if self.is_train else self.num_val_samples diff --git a/examples/mnist/datasets/dummy_dataset.py b/examples/mnist/datasets/dummy_dataset.py new file mode 100644 index 0000000..91b5524 --- /dev/null +++ b/examples/mnist/datasets/dummy_dataset.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +import torch +from typing import Tuple + + +class DummyDataset(Dataset): + """An iterator that produces repeated dummy data. + Args: + data_sample: a data sample that should be produced at each step. + batch_size: the batch size for storing. + sample_count: number of `data` samples in the dummy dataset. + """ + + def __init__(self, data_shape: torch.Size, num_classes: int, sample_count: int) -> None: + self._data_sample = torch.zeros(data_shape) + self._class_sample = torch.zeros((num_classes,), dtype=torch.int64) + self._sample_count = sample_count + + def __len__(self) -> int: + return self._sample_count + + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: + return self._data_sample, self._class_sample diff --git a/examples/mnist/datasets/mnist.py b/examples/mnist/datasets/mnist.py new file mode 100644 index 0000000..fde7f8e --- /dev/null +++ b/examples/mnist/datasets/mnist.py @@ -0,0 +1,23 @@ +from torch.utils.data import Dataset +from torchvision.datasets import mnist + +from .base import BasicDataset + + +class MNIST(BasicDataset): + name = "mnist" + num_classes = 10 + shape = (1, 1, 28, 28) + + mean = (0.1307,) + std_dev = (0.3081,) + num_train_samples = 60000 + num_val_samples = 10000 + + def get_dataset(self, download: bool = True) -> Dataset: + return mnist.MNIST( + root=self.root_directory, + train=self.is_train, + transform=self.get_transform(), + download=download, + ) diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 0d7ec46..4f5a18c 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -14,7 +14,8 @@ from torch.optim.lr_scheduler import StepLR import bitorch.layers as qnn -from bitorch import datasets as bitorch_datasets, RuntimeMode +from bitorch import RuntimeMode +from datasets import MNIST from bitorch.layers import convert import bitorch_inference_engine @@ -156,7 +157,7 @@ def main(): train_kwargs.update(cuda_kwargs) test_kwargs.update(cuda_kwargs) - train_dataset, test_dataset = bitorch_datasets.MNIST.get_train_and_test(download=True) + train_dataset, test_dataset = MNIST.get_train_and_test(download=True) train_loader = torch.utils.data.DataLoader(train_dataset, **train_kwargs) test_loader = torch.utils.data.DataLoader(test_dataset, **test_kwargs) From b43b3c289fb33a639d1753894bdabe7e41117b45 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:16:27 +0200 Subject: [PATCH 127/208] edit changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae9b4b..5fbf35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - code is now formatted with the black code formatter - using PyTorch's implementation of RAdam - renamed the `bitwidth` attribute of quantization functions to `bit_width` +- moved the image datasets out of the bitorch core package into the image classification example ### Fixed From d5cd6e852b1ca0f0aca844d905caa899e3ae8ac3 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:38:27 +0200 Subject: [PATCH 128/208] merged develop --- bitorch/models/common_layers.py | 5 +++-- bitorch/models/densenet.py | 2 +- bitorch/models/meliusnet.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index 74c04a1..81fa852 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -1,9 +1,10 @@ -from statistics import variance from typing import List, Optional, Union from torch import nn -def get_initial_layers(variant: Optional[Union[List[int], str]], input_channels: int, output_channels: int) -> List[nn.Module]: +def get_initial_layers( + variant: Optional[Union[List[int], str]], input_channels: int, output_channels: int +) -> List[nn.Module]: """returns the initial layers for the given variant""" layers: List[nn.Module] = [] if variant == (224, 224) or variant == "imagenet": diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index b8d5fe5..c4f13f6 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -226,8 +226,8 @@ class DenseNet(Model): def __init__( self, - input_shape: List[int], num_layers: Optional[int], + input_shape: List[int], num_classes: int = 0, num_init_features: int = 64, growth_rate: int = 64, diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index bb2340f..6a11aa1 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -72,8 +72,8 @@ class MeliusNet(Model): def __init__( self, - input_shape: List[int], num_layers: Optional[str], + input_shape: List[int], num_classes: int = 0, num_init_features: int = 64, growth_rate: int = 64, From d3012315a6af8131047f079b5aad93dc314e056e Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 12 Aug 2022 16:45:55 +0200 Subject: [PATCH 129/208] fixed model name --- bitorch/models/dlrm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitorch/models/dlrm.py b/bitorch/models/dlrm.py index 75adbf7..af4c4ad 100644 --- a/bitorch/models/dlrm.py +++ b/bitorch/models/dlrm.py @@ -100,7 +100,7 @@ def create_embeddings( class DLRM(Model): - name = "dlrm" + name = "DLRM" total_size = 1.0 inference_speed = 1.0 validation_results: List[dict] = [] @@ -112,7 +112,7 @@ def __init__( input_shape: List[int] = [], bottom_mlp_layer_sizes: Union[List[int], str] = [512, 256, 64], top_mlp_layer_sizes: Union[List[int], str] = [512, 256, 1], - interaction_operation: Interaction_Operation_Type = Interaction_Operation_Type.PRODUCT.value, + interaction_operation: str = Interaction_Operation_Type.PRODUCT.value, binary_bottom_mlp: bool = False, binary_top_mlp: bool = True, binary_embedding: bool = True, From 152b81d613413ff5d3c14251d9330ee4744286cd Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Sat, 13 Aug 2022 11:15:18 +0200 Subject: [PATCH 130/208] fixed model test --- tests/models/test_models.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 7ac3bb6..34960ab 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -89,12 +89,19 @@ def test_models(model_class, model_kwargs, datasets_to_test, dataset) -> None: if dataset is IMAGENET: batch_sizes_to_test = [1, 2] for batch_size in batch_sizes_to_test: - if model_class.name == "dlrm": - model = model_class(dense_feature_size=dataset[0][0][1], embedding_layer_sizes=[100] * dataset[0][1][0][0], **combination) + if model_class.name == "DLRM": + model = model_class( + dense_feature_size=dataset[0][0][1], + embedding_layer_sizes=[100] * dataset[0][1][0][0], + **combination, + ) dataset[0][0][0] = batch_size dataset[0][1][0][1] = batch_size dataset[0][1][1][1] = batch_size - input_values = (torch.Tensor(np.random.uniform(0, 1.0, dataset[0][0])), (torch.zeros(dataset[0][1][0], dtype=int), torch.zeros(dataset[0][1][1], dtype=int))) + input_values = ( + torch.Tensor(np.random.uniform(0, 1.0, dataset[0][0])), + (torch.zeros(dataset[0][1][0], dtype=int), torch.zeros(dataset[0][1][1], dtype=int)), + ) output = model(*input_values) else: input_shape = list(dataset[0]) From d3586e77dad5b549a3400762c720eeb0aefa73ed Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Sat, 13 Aug 2022 11:17:53 +0200 Subject: [PATCH 131/208] fixed sequential append error in compability test --- bitorch/models/densenet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 740fa69..d5ece13 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -17,7 +17,7 @@ def __init__(self, num_features: int, growth_rate: int, bn_size: int, dilation: super(DenseLayer, self).__init__() self.dropout = dropout self.num_features = num_features - self.features = nn.Sequential() + self.features = [] if bn_size == 0: # no bottleneck self._add_conv_block( @@ -26,6 +26,7 @@ def __init__(self, num_features: int, growth_rate: int, bn_size: int, dilation: else: self._add_conv_block(QConv2d(self.num_features, bn_size * growth_rate, kernel_size=1)) self._add_conv_block(QConv2d(bn_size * growth_rate, growth_rate, kernel_size=3, padding=1)) + self.features = nn.Sequential(*self.features) def _add_conv_block(self, layer: Module) -> None: self.features.append(nn.BatchNorm2d(self.num_features)) From 4d8d80f54a7ab23b14066faa54d85cdb98c0f7e6 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Sat, 13 Aug 2022 12:06:02 +0200 Subject: [PATCH 132/208] fixed bugs --- bitorch/models/densenet.py | 28 +++++++++++++++------------- bitorch/models/meliusnet.py | 9 ++++++--- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index d5ece13..37d2cab 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -17,7 +17,7 @@ def __init__(self, num_features: int, growth_rate: int, bn_size: int, dilation: super(DenseLayer, self).__init__() self.dropout = dropout self.num_features = num_features - self.features = [] + self.feature_list: List[Module] = [] if bn_size == 0: # no bottleneck self._add_conv_block( @@ -26,13 +26,13 @@ def __init__(self, num_features: int, growth_rate: int, bn_size: int, dilation: else: self._add_conv_block(QConv2d(self.num_features, bn_size * growth_rate, kernel_size=1)) self._add_conv_block(QConv2d(bn_size * growth_rate, growth_rate, kernel_size=3, padding=1)) - self.features = nn.Sequential(*self.features) + self.features = nn.Sequential(*self.feature_list) def _add_conv_block(self, layer: Module) -> None: - self.features.append(nn.BatchNorm2d(self.num_features)) - self.features.append(layer) + self.feature_list.append(nn.BatchNorm2d(self.num_features)) + self.feature_list.append(layer) if self.dropout: - self.features.append(nn.Dropout(self.dropout)) + self.feature_list.append(nn.Dropout(self.dropout)) def forward(self, x: torch.Tensor) -> torch.Tensor: ident = x @@ -107,31 +107,33 @@ def _make_transition(self, transition_num: int) -> None: num_out_features = self.num_features // self.reduction_rates[transition_num] num_out_features = int(round(num_out_features / 32)) * 32 - transition = nn.Sequential() + transition_layers: List[Module] = [] for layer in self.downsample_struct.split(","): if layer == "bn": - transition.append(nn.BatchNorm2d(self.num_features)) + transition_layers.append(nn.BatchNorm2d(self.num_features)) elif layer == "relu": - transition.append(nn.ReLU()) + transition_layers.append(nn.ReLU()) elif layer == "q_conv": - transition.append(QConv2d(self.num_features, num_out_features, kernel_size=1)) + transition_layers.append(QConv2d(self.num_features, num_out_features, kernel_size=1)) elif "fp_conv" in layer: groups = 1 if ":" in layer: groups = int(layer.split(":")[1]) - transition.append( + transition_layers.append( nn.Conv2d(self.num_features, num_out_features, kernel_size=1, groups=groups, bias=False) ) elif layer == "pool" and dilation == 1: - transition.append(nn.AvgPool2d(2, stride=2)) + transition_layers.append(nn.AvgPool2d(2, stride=2)) elif layer == "max_pool" and dilation == 1: - transition.append(nn.MaxPool2d(2, stride=2)) + transition_layers.append(nn.MaxPool2d(2, stride=2)) elif "cs" in layer: groups = 16 if ":" in layer: groups = int(layer.split(":")[1]) - transition.append(ChannelShuffle(groups)) + transition_layers.append(ChannelShuffle(groups)) + + transition = nn.Sequential(*transition_layers) self.features.add_module("Transition_%d" % (transition_num + 1), transition) self.num_features = num_out_features diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index df20c82..c9bd0bc 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -18,15 +18,18 @@ class ImprovementBlock(Module): def __init__(self, channels: int, in_channels: int, dilation: int = 1): super(ImprovementBlock, self).__init__() - self.body = nn.Sequential() - self.body.append(nn.BatchNorm2d(in_channels)) - self.body.append(QConv2d(in_channels, channels, kernel_size=3, stride=1, padding=dilation, dilation=dilation)) + self.body_layers: List[Module] = [] + self.body_layers.append(nn.BatchNorm2d(in_channels)) + self.body_layers.append( + QConv2d(in_channels, channels, kernel_size=3, stride=1, padding=dilation, dilation=dilation) + ) self.use_sliced_addition = channels != in_channels if self.use_sliced_addition: assert channels < in_channels self.slices = [0, in_channels - channels, in_channels] self.slices_add_x = [False, True] + self.body = nn.Sequential(*self.body_layers) def forward(self, x: torch.Tensor) -> torch.Tensor: residual = x From 5aa70c6356a8a2186135d116a165fffecb06ccbd Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 15 Aug 2022 11:28:00 +0200 Subject: [PATCH 133/208] fix grouped stem --- bitorch/models/common_layers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index 81fa852..f204121 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -12,15 +12,13 @@ def get_initial_layers( elif variant == "grouped_stem": stem_width = output_channels // 2 - layers.append(nn.Conv2D(input_channels, stem_width, kernel_size=3, strides=2, padding=1, use_bias=False)) + layers.append(nn.Conv2d(input_channels, stem_width, kernel_size=3, stride=2, padding=1, bias=False)) layers.append(nn.BatchNorm2d(stem_width, momentum=0.9)) layers.append(nn.ReLU()) - layers.append(nn.Conv2D(stem_width, stem_width, kernel_size=3, strides=1, padding=1, groups=4, use_bias=False)) + layers.append(nn.Conv2d(stem_width, stem_width, kernel_size=3, stride=1, padding=1, groups=4, bias=False)) layers.append(nn.BatchNorm2d(stem_width, momentum=0.9)) layers.append(nn.ReLU()) - layers.append( - nn.Conv2D(stem_width, stem_width * 2, kernel_size=3, strides=1, padding=1, groups=8, use_bias=False) - ) + layers.append(nn.Conv2d(stem_width, output_channels, kernel_size=3, stride=1, padding=1, groups=8, bias=False)) else: layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=3, padding=1, bias=False)) From fc37629613d1ec3bdaa1ba03123c901812cc5f3e Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 15 Aug 2022 14:47:49 +0200 Subject: [PATCH 134/208] actuall check future compatibility --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a47aef5..90ac08a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ image: python:3.7 before_script: - python --version - - pip install -e ".[dev]" --extra-index-url https://download.pytorch.org/whl/cu113 + - pip install -e ".[dev]" .scheduled-only: rules: @@ -61,7 +61,7 @@ test:torch-compatibility: extends: - .test script: - - pip install torch==1.9.0 torchvision==0.10.0 --extra-index-url https://download.pytorch.org/whl/cu113 + - pip install torch~=1.12.0 torchvision~=0.13.0 - pytest --version - python -m pytest . From 7718cf6bf29ceb94e5d4f01ed41f9b7922115f9b Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 8 Jul 2022 10:36:04 +0200 Subject: [PATCH 135/208] add real train accuracy, and correct previous train as batch accuracy, change progressive sign transform --- bitorch/quantizations/progressive_sign.py | 2 +- examples/pytorch_lightning/utils/lightning_model.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index ee477b8..5f8c85e 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -89,7 +89,7 @@ def current_scale(self) -> float: @staticmethod def default_transform(x: float) -> float: - return 1 - (5 ** (-3 * x)) + return 1 - (2 ** (-10 * x)) def transform(self, x: float) -> float: """Transform x for a steady temperature increase, higher at the beginning, and much less at the end.""" diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index ba6e320..fadd53a 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -25,6 +25,8 @@ def __init__( self.save_hyperparameters(clean_hyperparameters(script_args)) self.loss_function = CrossEntropyLoss() self.model = model + self.batch_accuracy_top1 = Accuracy(num_classes=num_classes) + self.batch_accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) self.train_accuracy_top1 = Accuracy(num_classes=num_classes) self.train_accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) self.accuracy_top1 = Accuracy(num_classes=num_classes) @@ -40,12 +42,16 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore y_hat = self.model(x_train) loss = self.calculate_loss(x_train, y_train, y_hat) + + self.batch_accuracy_top1(y_hat, y_train) + self.batch_accuracy_top5(y_hat, y_train) self.train_accuracy_top1(y_hat, y_train) self.train_accuracy_top5(y_hat, y_train) + self.log_dict( { - "metrics/train-top1-accuracy": self.train_accuracy_top1, - "metrics/train-top5-accuracy": self.train_accuracy_top5, + "metrics/batch-top1-accuracy": self.batch_accuracy_top1, + "metrics/batch-top5-accuracy": self.batch_accuracy_top5, }, prog_bar=True, on_step=True, @@ -69,6 +75,8 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: metrics_dict = { "metrics/test-top1-accuracy": self.accuracy_top1, "metrics/test-top5-accuracy": self.accuracy_top5, + "metrics/train-top1-accuracy": self.train_accuracy_top1, + "metrics/train-top5-accuracy": self.train_accuracy_top5, "loss/test": loss, } From 8096e0f8568df6b251e26479d94df65ede9178e5 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 8 Jul 2022 13:40:56 +0200 Subject: [PATCH 136/208] correct train and batch accuracy --- examples/pytorch_lightning/image_classification.py | 2 +- examples/pytorch_lightning/utils/lightning_model.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 43b2dc9..13df977 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -132,7 +132,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: limit_val_batches=0.01 if args.dev_run else None, ) if args.dev_run: - logger.info("This run only uses 1 % of training and validation data (--dev-run)!") + logger.info("this run only uses 1 % of training and validation data (--dev-run)!") logger.info(f"model: {args.model}") logger.info(f"optimizer: {args.optimizer}") logger.info(f"lr: {args.lr}") diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index fadd53a..225cb36 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -52,12 +52,20 @@ def training_step(self, batch: torch.Tensor) -> torch.Tensor: # type: ignore { "metrics/batch-top1-accuracy": self.batch_accuracy_top1, "metrics/batch-top5-accuracy": self.batch_accuracy_top5, + "loss/train": loss, }, prog_bar=True, on_step=True, on_epoch=False, ) - self.log("loss/train", loss, on_step=True, on_epoch=False) + self.log_dict( + { + "metrics/train-top1-accuracy": self.train_accuracy_top1, + "metrics/train-top5-accuracy": self.train_accuracy_top5, + }, + on_step=False, + on_epoch=True, + ) return loss def calculate_loss(self, x_train: torch.Tensor, y_train: torch.Tensor, y_hat: torch.Tensor) -> torch.Tensor: @@ -75,8 +83,6 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: metrics_dict = { "metrics/test-top1-accuracy": self.accuracy_top1, "metrics/test-top5-accuracy": self.accuracy_top5, - "metrics/train-top1-accuracy": self.train_accuracy_top1, - "metrics/train-top5-accuracy": self.train_accuracy_top5, "loss/test": loss, } From 8cc76d920efd48f6b54783ffebec3e4ada0d0188 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Sat, 9 Jul 2022 17:47:29 +0200 Subject: [PATCH 137/208] make params of progressive sign function configurable, update tests --- bitorch/quantizations/config.py | 6 ++++ bitorch/quantizations/progressive_sign.py | 40 +++++++++++++++++++---- tests/quantizations/test_quantizations.py | 4 +-- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/bitorch/quantizations/config.py b/bitorch/quantizations/config.py index d15c3e6..c527a5c 100644 --- a/bitorch/quantizations/config.py +++ b/bitorch/quantizations/config.py @@ -13,5 +13,11 @@ class QuantizationConfig(Config): # scaling of progressive sign function, should be zero at the start of the training, and (close to) one at the end progressive_sign_scale = 0.0 + # alpha of default progressive sign transform function, should be between 2 and 10 + progressive_sign_alpha = 2 + + # beta of default progressive sign transform function, should be between 2 and 10 + progressive_sign_beta = 10 + config = QuantizationConfig() diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index 5f8c85e..3ebf260 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -1,6 +1,6 @@ """Progressive Sign Function""" import typing -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Union import torch import torch.nn.functional as F @@ -53,12 +53,16 @@ class ProgressiveSign(Quantization): scale: float global_scaling: bool + alpha: Union[int, float] + beta: Union[int, float] def __init__( self, use_global_scaling: bool = True, initial_scale: Optional[float] = None, custom_transform: Optional[Callable[[float], float]] = None, + alpha: Union[int, float] = None, + beta: Union[int, float] = None, ) -> None: """ Initialize the progressive sign module (can be used for progressive weight binarization). @@ -70,6 +74,8 @@ def __init__( use_global_scaling: whether to use the global scaling variable stored in the config initial_scale: if not using global scaling you can set an initial scale custom_transform: to use a custom transform function from scale to temperature, add it here + alpha: parameters of default transform function + beta: parameters of default transform function """ super().__init__() if initial_scale is not None and use_global_scaling: @@ -80,22 +86,42 @@ def __init__( self.global_scaling = use_global_scaling self.scale = initial_scale or config.progressive_sign_scale self.custom_transform = custom_transform + self.alpha = alpha or config.progressive_sign_alpha + self.beta = beta or config.progressive_sign_beta @property def current_scale(self) -> float: + """Return the current scale of this Progressive Sign layer.""" if self.global_scaling: return config.progressive_sign_scale return self.scale @staticmethod - def default_transform(x: float) -> float: - return 1 - (2 ** (-10 * x)) + def default_transform(scale: float, alpha: Union[int, float] = None, beta: Union[int, float] = None) -> float: + """Transform the given scale into the temperature of the progressive sign function with the default function. - def transform(self, x: float) -> float: - """Transform x for a steady temperature increase, higher at the beginning, and much less at the end.""" + The formula is as follows: 1 - (alpha ** (-beta * scale)) + + Args: + scale: the current scale + alpha: base of default exponential function + beta: factor of scale exponent + """ + if alpha is None: + alpha = config.progressive_sign_alpha + if beta is None: + beta = config.progressive_sign_alpha + return 1 - (alpha ** (-beta * scale)) + + def transform(self, scale: float) -> float: + """Transform the given scale into a steady temperature increase, higher at the start, and much less at the end. + + Args: + scale: the current scale + """ if self.custom_transform is not None: - return self.custom_transform(x) - return self.default_transform(x) + return self.custom_transform(scale) + return self.default_transform(scale, self.alpha, self.beta) def quantize(self, x: torch.Tensor) -> torch.Tensor: """Forwards the tensor through the sign function. diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index 502655a..fd97c04 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -31,14 +31,14 @@ ProgressiveSign(use_global_scaling=False, initial_scale=0.2), 1, [-1.5, -1.0, -0.3, -0.1, 0.0, 0.1, 0.3, 1.0, 1.5], - [-1.0, -1.0, -0.788, -0.2627, 0.0, 0.2627, 0.788, 1.0, 1.0], + [-1.0, -1.0, -1.0, -0.4, 0.0, 0.4, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ( ProgressiveSign(use_global_scaling=False, initial_scale=0.5), 1, [-1.5, -1.0, -0.3, -0.1, -0.05, 0.0, 0.05, 0.1, 0.3, 1.0, 1.5], - [-1.0, -1.0, -1.0, -1.0, -0.559, 0.0, 0.559, 1.0, 1.0, 1.0, 1.0], + [-1.0, -1.0, -1.0, -1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ( From ffe440cb7f986420567430e843004c191326fdae Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Sun, 10 Jul 2022 11:28:26 +0200 Subject: [PATCH 138/208] fix getting default param --- bitorch/quantizations/progressive_sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index 3ebf260..dbb47da 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -110,7 +110,7 @@ def default_transform(scale: float, alpha: Union[int, float] = None, beta: Union if alpha is None: alpha = config.progressive_sign_alpha if beta is None: - beta = config.progressive_sign_alpha + beta = config.progressive_sign_beta return 1 - (alpha ** (-beta * scale)) def transform(self, scale: float) -> float: From da35ec2aa2e42c88da9659e2cb446a6c57efdbd7 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 22 Jul 2022 17:33:24 +0200 Subject: [PATCH 139/208] apply rename of bitorch engine and add integration options for custom models, quantizations and datasets --- bitorch/__init__.py | 15 ++++++++++--- bitorch/layers/config.py | 2 +- bitorch/models/__init__.py | 22 +++++++++++++++---- bitorch/quantizations/__init__.py | 17 ++++++++++++-- bitorch/quantizations/config.py | 9 -------- bitorch/quantizations/progressive_sign.py | 20 +++++++++++++++-- bitorch/quantizations/sign.py | 4 ++-- examples/mnist/requirements.txt | 2 +- examples/mnist/train_mnist.py | 4 ++-- .../pytorch_lightning/utils/arg_parser.py | 7 +++--- examples/pytorch_lightning/utils/callbacks.py | 4 ++-- version.txt | 2 +- 12 files changed, 75 insertions(+), 33 deletions(-) diff --git a/bitorch/__init__.py b/bitorch/__init__.py index cb6bb45..6d63335 100644 --- a/bitorch/__init__.py +++ b/bitorch/__init__.py @@ -57,7 +57,7 @@ def config_from_name(name: str) -> Config: def config_names() -> List: - """getter for list of config names for argparse + """Get the list of config names for argparse. Returns: List: the config names @@ -66,7 +66,7 @@ def config_names() -> List: def add_config_args(parser: ArgumentParser) -> None: - """adds all config arguments + """Adds all arguments from all registered configs. Args: parser (ArgumentParser): parser to add the arguments to @@ -76,10 +76,19 @@ def add_config_args(parser: ArgumentParser) -> None: def apply_args_to_configuration(args: Namespace) -> None: - """applys the cli configurations to the config objects. + """Applies the cli configurations to the config objects. Args: args (Namespace): the cli configurations """ for config_ in configs_by_name.values(): config_.apply_args_to_configuration(args) + + +def register_custom_config(custom_config: Config) -> None: + """Register a custom (external) config in bitorch. + + Args: + custom_config: the custom config which should be added to bitorch + """ + configs_by_name[custom_config.name] = custom_config diff --git a/bitorch/layers/config.py b/bitorch/layers/config.py index 05b34c9..17f393f 100644 --- a/bitorch/layers/config.py +++ b/bitorch/layers/config.py @@ -12,7 +12,7 @@ class LayerConfig(Config): name = "layer_config" def get_quantization_function(self, quantization: Union[str, Quantization]) -> Quantization: - """Returns the quantization module specified in quantization_name. + """Returns the quantization module specified by the given name or object. Args: quantization: quantization module or name of quantization function. diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index 1ce4fa7..fc02ccc 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -47,6 +47,9 @@ __all__ = [ "Model", + "model_from_name", + "model_names", + "register_custom_model", "LeNet", "Resnet", "Resnet152V1", @@ -81,8 +84,8 @@ def model_from_name(name: str) -> Type[Model]: - """returns the model to which the name belongs to (name has to be the value of the models - name-attribute) + """ + Return a model by the given name. Args: name (str): name of the model @@ -98,10 +101,21 @@ def model_from_name(name: str) -> Type[Model]: return models_by_name[name.lower()] -def model_names() -> List: - """getter for list of model names for argparse +def model_names() -> List[str]: + """ + Get the list of model names. Returns: List: the model names """ return list(models_by_name.keys()) + + +def register_custom_model(custom_model: Type[Model]) -> None: + """ + Register a custom (external) model in bitorch. + + Args: + custom_model: the custom model which should be added to bitorch + """ + models_by_name[custom_model.name] = custom_model diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index cdae11f..0401d0a 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -5,7 +5,7 @@ If you want to implement a new function, use the :code:`Quantization` base class as superclass. """ -from typing import List, Type +from typing import List, Type, Dict from .base import Quantization from .approx_sign import ApproxSign @@ -19,6 +19,9 @@ __all__ = [ "Quantization", + "quantization_from_name", + "quantization_names", + "register_custom_quantization", "ApproxSign", "InputDoReFa", "WeightDoReFa", @@ -30,7 +33,7 @@ ] -quantizations_by_name = build_lookup_dictionary(__name__, __all__, Quantization) +quantizations_by_name: Dict[str, Type[Quantization]] = build_lookup_dictionary(__name__, __all__, Quantization) def quantization_from_name(name: str) -> Type[Quantization]: @@ -58,3 +61,13 @@ def quantization_names() -> List: List: the quantization names """ return list(quantizations_by_name.keys()) + + +def register_custom_quantization(custom_quantization: Type[Quantization]) -> None: + """ + Register a custom (external) quantization in bitorch. + + Args: + custom_quantization: the custom config which should be added to bitorch + """ + quantizations_by_name[custom_quantization.name] = custom_quantization diff --git a/bitorch/quantizations/config.py b/bitorch/quantizations/config.py index c527a5c..ad1f6a5 100644 --- a/bitorch/quantizations/config.py +++ b/bitorch/quantizations/config.py @@ -10,14 +10,5 @@ class QuantizationConfig(Config): # beta value for swishsign function beta = 5.0 - # scaling of progressive sign function, should be zero at the start of the training, and (close to) one at the end - progressive_sign_scale = 0.0 - - # alpha of default progressive sign transform function, should be between 2 and 10 - progressive_sign_alpha = 2 - - # beta of default progressive sign transform function, should be between 2 and 10 - progressive_sign_beta = 10 - config = QuantizationConfig() diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index dbb47da..384fd48 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -5,13 +5,29 @@ import torch import torch.nn.functional as F +from bitorch.config import Config from .base import Quantization, STE -from .config import config from .sign import SignFunction EPSILON = 1e-7 +class ProgressiveSignConfig(Config): + name = "progressive_sign_config" + + # scaling of progressive sign function, should be zero at the start of the training, and (close to) one at the end + progressive_sign_scale = 0.0 + + # alpha of default progressive sign transform function, should be between 2 and 10 + progressive_sign_alpha = 2 + + # beta of default progressive sign transform function, should be between 2 and 10 + progressive_sign_beta = 10 + + +config = ProgressiveSignConfig() + + class ProgressiveSignFunctionTrain(STE): @staticmethod @typing.no_type_check @@ -105,7 +121,7 @@ def default_transform(scale: float, alpha: Union[int, float] = None, beta: Union Args: scale: the current scale alpha: base of default exponential function - beta: factor of scale exponent + beta: (negative) factor of scale exponent """ if alpha is None: alpha = config.progressive_sign_alpha diff --git a/bitorch/quantizations/sign.py b/bitorch/quantizations/sign.py index 2cdb88d..99d45e7 100644 --- a/bitorch/quantizations/sign.py +++ b/bitorch/quantizations/sign.py @@ -14,7 +14,7 @@ def forward( ctx: torch.autograd.function.BackwardCFunction, # type: ignore input_tensor: torch.Tensor, ) -> torch.Tensor: - """Binarize the input tensor using the sign function + """Binarize the input tensor using the sign function. Args: ctx (Any): autograd context @@ -29,7 +29,7 @@ def forward( class Sign(Quantization): - """Module for applying the sign function with straight through estimator in backward pass""" + """Module for applying the sign function with straight through estimator in backward pass.""" name = "sign" bit_width = 1 diff --git a/examples/mnist/requirements.txt b/examples/mnist/requirements.txt index 96c05b9..ababd87 100644 --- a/examples/mnist/requirements.txt +++ b/examples/mnist/requirements.txt @@ -1,2 +1,2 @@ bitorch -bitorch_inference_engine +bitorch_engine diff --git a/examples/mnist/train_mnist.py b/examples/mnist/train_mnist.py index 4f5a18c..5eb9480 100644 --- a/examples/mnist/train_mnist.py +++ b/examples/mnist/train_mnist.py @@ -17,10 +17,10 @@ from bitorch import RuntimeMode from datasets import MNIST from bitorch.layers import convert -import bitorch_inference_engine +import bitorch_engine -bitorch_inference_engine.initialize() +bitorch_engine.initialize() class QuantizedMLP(nn.Module): diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 840a8b1..ea1b754 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -5,11 +5,10 @@ from pytorch_lightning import Trainer -from bitorch import add_config_args -from ..datasets import dataset_names # type: ignore from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin -from examples.pytorch_lightning.utils.teachers import available_teachers +from ..datasets import dataset_names +from ..utils.teachers import available_teachers class _HeadArgumentParser(ArgumentParser): @@ -303,7 +302,7 @@ def add_regular_args(parser: ArgumentParser) -> None: add_checkpoint_args(parser) add_training_args(parser) - add_config_args(parser) + bitorch.add_config_args(parser) parser.add_argument( "--model", diff --git a/examples/pytorch_lightning/utils/callbacks.py b/examples/pytorch_lightning/utils/callbacks.py index 9bda972..f2be7a3 100644 --- a/examples/pytorch_lightning/utils/callbacks.py +++ b/examples/pytorch_lightning/utils/callbacks.py @@ -1,7 +1,7 @@ import pytorch_lightning as pl from bitorch.quantizations import ProgressiveSign -from bitorch.quantizations.config import config as quantization_config +from bitorch.quantizations.progressive_sign import config as progressive_sign_config class ProgressiveSignScalerCallback(pl.callbacks.Callback): @@ -9,7 +9,7 @@ class ProgressiveSignScalerCallback(pl.callbacks.Callback): def on_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: scale = trainer.current_epoch / trainer.max_epochs - quantization_config.progressive_sign_scale = scale + progressive_sign_config.progressive_sign_scale = scale for logger in trainer.loggers: logger.log_metrics( { diff --git a/version.txt b/version.txt index 8a3ba5a..b9f6376 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.0.dev1 +0.3.0.dev2 From 8af0a67560f6f46ef957f35c1c61486bd6b1446a Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 22 Jul 2022 17:34:39 +0200 Subject: [PATCH 140/208] add changelog --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ae0ee..4d09a27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - support for integration of bitorch's inference engine for the following layers - QLinear - QConv -- New quantization function: [Progressive Sign](bitorch/quantizations/progressive_sign.py) -- New features in PyTorch Lightning example: - - Training with Knowledge Distillation - - Improved Logging - - Callback to update Progressive Sign modules +- new quantization function: [Progressive Sign](bitorch/quantizations/progressive_sign.py) +- new features in PyTorch Lightning example: + - training with Knowledge Distillation + - improved logging + - callback to update Progressive Sign module +- option to integrate custom models, datasets, quantization functions ### Changed From d8bd77923f052b08864c07892ba1aa2f3d58f137 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 15 Aug 2022 11:33:37 +0200 Subject: [PATCH 141/208] increase prog sign alpha --- bitorch/quantizations/progressive_sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index 384fd48..bdeb0ce 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -19,7 +19,7 @@ class ProgressiveSignConfig(Config): progressive_sign_scale = 0.0 # alpha of default progressive sign transform function, should be between 2 and 10 - progressive_sign_alpha = 2 + progressive_sign_alpha = 4 # beta of default progressive sign transform function, should be between 2 and 10 progressive_sign_beta = 10 From c2d78453cfccbbd1c764c631286bc6254a2a1741 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Mon, 15 Aug 2022 11:33:44 +0200 Subject: [PATCH 142/208] fix argparse imports --- examples/pytorch_lightning/utils/arg_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index ea1b754..a201c5c 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -5,6 +5,7 @@ from pytorch_lightning import Trainer +import bitorch from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin from ..datasets import dataset_names From 72c4b6e3899fb99167121da9f205c83739e00bee Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 16 Aug 2022 11:59:47 +0200 Subject: [PATCH 143/208] fix test --- tests/quantizations/test_quantizations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index fd97c04..e71ac77 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -28,10 +28,10 @@ [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ( - ProgressiveSign(use_global_scaling=False, initial_scale=0.2), + ProgressiveSign(use_global_scaling=False, initial_scale=0.05), 1, [-1.5, -1.0, -0.3, -0.1, 0.0, 0.1, 0.3, 1.0, 1.5], - [-1.0, -1.0, -1.0, -0.4, 0.0, 0.4, 1.0, 1.0, 1.0], + [-1.0, -1.0, -0.6, -0.2, 0.0, 0.2, 0.6, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ( @@ -117,6 +117,7 @@ def test_quantizations( assert torch.allclose(y, x_exp, atol=0.001) assert quantization.bit_width == bits with pytest.deprecated_call(): + # noinspection PyDeprecation assert quantization.bitwidth == bits y.backward(x) From 817bfddbe0e507dd8dfc6f9ed5382fef7d6d9a87 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Tue, 16 Aug 2022 11:36:09 +0200 Subject: [PATCH 144/208] adapt ci --- .gitlab-ci.yml | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90ac08a..75393cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,8 @@ before_script: - python --version - pip install -e ".[dev]" -.scheduled-only: +# jobs extending .scheduled_only only run in scheduled pipelines not on every commit +.scheduled_only: rules: - if: $CI_PIPELINE_SOURCE == "schedule" @@ -21,26 +22,19 @@ before_script: - black . --check --verbose --diff --color codestyle: - extends: .codestyle + extends: + - .codestyle codestyle:3.10: extends: - .codestyle - - .scheduled-only + - .scheduled_only image: python:3.10 # Running tests -.test: - stage: test +test_and_coverage: script: - - pytest --version - - python -m pytest . - -test-and-coverage: - extends: .test - script: - - pytest --version - coverage run -m pytest - coverage report - coverage xml @@ -53,29 +47,28 @@ test-and-coverage: test:3.10: extends: - - .test - - .scheduled-only + - .scheduled_only image: python:3.10 + script: + - python -m pytest . -test:torch-compatibility: - extends: - - .test +test:torch_backwards_compatibility: script: - - pip install torch~=1.12.0 torchvision~=0.13.0 - - pytest --version + - pip install torch==1.9.0 torchvision==0.10.0 - python -m pytest . # Documentation -test-build-doc: +test_build_doc: stage: test script: - apt-get update && apt-get install -y pandoc - sphinx-build -b html docs/source/ docs/build/ -a -test-doc-completeness: - extends: .scheduled-only +test_doc_completeness: + extends: .scheduled_only stage: test + allow_failure: true script: - flake8 --version # explicitly select Docstring errors and ignore to overwrite config in setup.cfg From 58d3006a1c9a05c12338ff10efafd7fef099d0cf Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 19 Aug 2022 17:32:18 +0200 Subject: [PATCH 145/208] adapted readme for dlrm --- examples/dlrm/README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/examples/dlrm/README.md b/examples/dlrm/README.md index 9b4d4cd..42c2af8 100644 --- a/examples/dlrm/README.md +++ b/examples/dlrm/README.md @@ -1,7 +1,7 @@ # Pytorch Lightning Example Script -To give an example on how to use bitorch for your own projects `image_classification.py` trains one of the -models implemented in `bitorch` on an image classification dataset. +To give an example on how to use bitorch for your own recommendation projects `train_dlrm.py` trains a quantized version of Facebooks [DLRM](https://github.com/facebookresearch/dlrm) implemented in `bitorch` on an ad recommendation dataset. +Right now only the [Criteo Ad Challenge](https://labs.criteo.com/2014/02/kaggle-display-advertising-challenge-dataset/) dataset is supported. First the requirements for this example need to be installed (unless the optional dependencies of BITorch were already installed): @@ -11,12 +11,14 @@ pip install -r requirements.txt Below you can find an example call of the script: ```bash -python3 image_classification.py --optimizer adam --lr 0.001 --lr-scheduler cosine --max_epochs 2 --dataset imagenet --model resnet18v1 --batch-size 128 --accelerator gpu --num-workers 16 --gpus 3 +python examples/dlrm/train_dlrm.py --dataset criteo --input-quantization sign --weight-quantization approxsign --download --ignore-dataset-size 0.0 --batch-size 8192 --lr-scheduler cosine --optimizer adam --wandb --batch-size-test 10000 --num-workers 0 --dataset-dir /datasets --gpus 1 --max_epochs 10 ``` +If the dataset is not present in the given directory, it will be downloaded to the specified directory and preprocessed. Preprocessing usually takes about 30 min, depending on your hardware setup. + ## Arguments -To find an exhaustive overview over the parameters to configure the `image_classification.py` script, call `python image_classification.py --help`. +To find an exhaustive overview over the parameters to configure the `train_dlrm.py` script, call `python train_dlrm.py --help`. The list below gives a brief overview over some selected arguments. ### general training args @@ -40,16 +42,10 @@ The list below gives a brief overview over some selected arguments. - `--checkpoint-dir` path to where checkpoints shall be stored - `--checkpoint-load` path to checkpoint to load from -### model args - -- `--model` specify name of model you want to train. Choose from `lenet,resnet,resnet152v1,resnet152v2,resnet18v1,resnet18v2,resnet34v1,resnet34v2,resnet50v1,resnet50v2,resnete,resnete18` or `resnete34` - -Each model can have specific arguments. Check them by calling `python image_classification.py --help`. - ### dataset args -- `--datset` name of dataset to train on. Chose from `mnist, cifar10, cifar100` and `imagenet` -- `--download` toggles if dataset if not present at `--dataset-dir` should be downloaded. Only available for `mnist` and `cifar10`. +- `--datset` name of dataset to train on. Chose from `criteo` +- `--download` toggles if dataset if not present at `--dataset-dir` should be downloaded. - `--dataset-dir` path to dataset. - `--num-worker` sets number of workers for dataloading From 9a70501eace6222ab51e3a1786a9335426f64ebf Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 17:03:53 +0200 Subject: [PATCH 146/208] added scheduler, not working yet --- bitorch/quantizations/__init__.py | 1 + examples/pytorch_lightning/datasets/base.py | 2 +- .../pytorch_lightning/image_classification.py | 12 +++++++++-- .../pytorch_lightning/utils/arg_parser.py | 20 +++++++++++++++++-- .../utils/lightning_model.py | 9 +++++++++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 0401d0a..4cea15c 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -15,6 +15,7 @@ from .ste_heaviside import SteHeaviside from .swish_sign import SwishSign from .progressive_sign import ProgressiveSign +from .quantization_scheduler import Quantization_Scheduler from ..util import build_lookup_dictionary __all__ = [ diff --git a/examples/pytorch_lightning/datasets/base.py b/examples/pytorch_lightning/datasets/base.py index c48bf18..7b76b71 100644 --- a/examples/pytorch_lightning/datasets/base.py +++ b/examples/pytorch_lightning/datasets/base.py @@ -7,7 +7,7 @@ from torch.utils.data import Dataset from torchvision.transforms import transforms -from ..datasets.dummy_dataset import DummyDataset +from datasets.dummy_dataset import DummyDataset class BasicDataset(Dataset): diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 13df977..31e679b 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -25,6 +25,7 @@ import bitorch from bitorch import apply_args_to_configuration, RuntimeMode +from bitorch.quantizations import Quantization_Scheduler, Sign, Identity, quantization_from_name from datasets import dataset_from_name from bitorch.models import model_from_name from bitorch.quantizations import Quantization @@ -101,9 +102,16 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - model = model_from_name(args.model)(**model_kwargs, input_shape=dataset.shape, num_classes=dataset.num_classes) # type: ignore + model = model_from_name(args.model)(**model_kwargs, input_shape=dataset.shape, + num_classes=dataset.num_classes) # type: ignore model.initialize() + if args.quantization_scheduling: + quantization_scheduler = Quantization_Scheduler(model, quantizations=[quantization_from_name( + name)() for name in args.scheduled_quantizations], scheduling_procedure=args.quantization_scheduling_procedure, steps=args.max_epochs) + else: + quantization_scheduler = None + wrapper_class: Type[ModelWrapper] = ModelWrapper if args.teacher: if args.dataset != "imagenet": @@ -117,7 +125,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = wrapper_class.load_from_checkpoint(args.checkpoint_load) else: - model_wrapped = wrapper_class(model, dataset.num_classes, args) + model_wrapped = wrapper_class(model, dataset.num_classes, quantization_scheduler, args) trainer = Trainer( strategy=args.strategy, diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index a201c5c..9aca8a9 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -8,8 +8,9 @@ import bitorch from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin -from ..datasets import dataset_names -from ..utils.teachers import available_teachers +from bitorch.quantizations.quantization_scheduler import Quantization_Scheduler +from datasets import dataset_names +from utils.teachers import available_teachers class _HeadArgumentParser(ArgumentParser): @@ -142,6 +143,21 @@ def add_optimizer_args(parser: ArgumentParser) -> None: choices=["cosine", "step", "exponential"], help="name of the lr scheduler to use. default to none", ) + optimizer.add_argument( + "--quantization-scheduling", + action="store_true", default=False, + help="toggles weather to use quantization scheduling", + ) + optimizer.add_argument( + "--scheduled-quantizations", + nargs="*", default=None, + help="name of quantizations to schedule", + ) + optimizer.add_argument( + "--quantization-scheduling-procedure", + type=str, default=None, choices=list(Quantization_Scheduler.procedure_classes.keys()), + help="procedure to use for scheduling", + ) optimizer.add_argument( "--lr", type=float, diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 225cb36..eee5efe 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -18,6 +18,7 @@ def __init__( self, model: Module, num_classes: int, + quantization_scheduler: Module, script_args: Namespace, add_f1_prec_recall: bool = False, ) -> None: @@ -32,6 +33,7 @@ def __init__( self.accuracy_top1 = Accuracy(num_classes=num_classes) self.accuracy_top5 = Accuracy(top_k=5, num_classes=num_classes) self.add_f1_prec_recall = add_f1_prec_recall + self.quantization_scheduler = quantization_scheduler if add_f1_prec_recall: self.f1 = F1Score(num_classes=num_classes) self.prec = Precision(num_classes=num_classes) @@ -101,6 +103,13 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: return loss + def on_epoch_end(self) -> None: + if self.quantization_scheduler is not None: + self.quantization_scheduler.step() + self.log("quantization_scheduler/mix_factor", + self.quantization_scheduler.scheduled_quantizer_instances[0].mix_factor) + return super().on_epoch_end() + def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: ignore logging.info(f"Using {self.hparams.optimizer} optimizer and {self.hparams.lr_scheduler} lr scheduler...") optimizer = create_optimizer(self.hparams.optimizer, self.model, self.hparams.lr, self.hparams.momentum) From 1db3a1b8b9011a9793fa0cf8f26cab14ab92ede4 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 17:04:14 +0200 Subject: [PATCH 147/208] added file --- .../quantizations/quantization_scheduler.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 bitorch/quantizations/quantization_scheduler.py diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py new file mode 100644 index 0000000..86adb4a --- /dev/null +++ b/bitorch/quantizations/quantization_scheduler.py @@ -0,0 +1,79 @@ +import sched +from torch.nn import Module +import torch +from typing import List, Type +from .base import Quantization +from copy import deepcopy +from .config import config + + +class MixLinearScheduling(Quantization): + name = "__mixlinarscheduling__" + + def __init__(self, quantizations, steps): + super().__init__() + self.quantizations = [deepcopy(quantization) for quantization in quantizations] + self.mix_factor = 0.0 + self.step_count = 0 + self.steps = steps + + def step(self): + print("before:", self.step_count, self.steps) + self.step_count += 1 + self.mix_factor = self.step_count / self.steps + print("after:", self.step_count, self.steps) + + assert self.mix_factor <= 1 + + def quantize(self, x: torch.Tensor): + if len(self.quantizations) == 1: + return self.quantizations[0](x) + scaled_mix_factor = self.mix_factor * (len(self.quantizations) - 1) + lower_idx = int(scaled_mix_factor) + higher_idx = lower_idx + 1 + if higher_idx == len(self.quantizations): + return self.quantizations[lower_idx](x) + + inter_unit_mix_factor = scaled_mix_factor - lower_idx + return self.quantizations[higher_idx](x) * inter_unit_mix_factor + self.quantizations[lower_idx](x) * (1.0 - inter_unit_mix_factor) + + +class Quantization_Scheduler(Module): + + procedure_classes = { + "mix_linear": MixLinearScheduling, + } + + def __init__(self, model, steps, quantizations: List[Quantization], scheduling_procedure: str, exclude_layers: List[Type] = []): + super().__init__() + + assert steps > 0, "steps has to be an integer > 0" + assert isinstance(quantizations, list) + assert len(quantizations) > 0 + + self.quantizations = quantizations + self.steps = steps + + self.scheduled_quantizer = self.get_scheduled_quantizer(scheduling_procedure) + + self.scheduled_quantizer_instances = [] + self.replace_quantizations(model, exclude_layers, "") + print("got ", len(self.scheduled_quantizer_instances), "quantization schedulers") + + def get_scheduled_quantizer(self, procedure): + return self.procedure_classes[procedure] + + def replace_quantizations(self, model, exclude_layers, module_name): + for name in dir(model): + module = getattr(model, name) + if issubclass(type(module), Quantization): + print("replaced", module_name, " -> ", name) + self.scheduled_quantizer_instances.append(self.scheduled_quantizer(self.quantizations, self.steps)) + setattr(model, name, self.scheduled_quantizer_instances[-1]) + + for name, children in model.named_children(): + self.replace_quantizations(children, exclude_layers, name) + + def step(self): + for scheduled_quantizer in self.scheduled_quantizer_instances: + scheduled_quantizer.step() From efb335dd2514d1af9fcce515ac3c8c189f3278fd Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 20:45:25 +0200 Subject: [PATCH 148/208] fixed bitwidth bug --- .../quantizations/quantization_scheduler.py | 18 ++++++++---------- examples/pytorch_lightning/utils/arg_parser.py | 4 ++-- .../pytorch_lightning/utils/lightning_model.py | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py index 86adb4a..0dd9bef 100644 --- a/bitorch/quantizations/quantization_scheduler.py +++ b/bitorch/quantizations/quantization_scheduler.py @@ -9,21 +9,20 @@ class MixLinearScheduling(Quantization): name = "__mixlinarscheduling__" + bit_width = 32 def __init__(self, quantizations, steps): super().__init__() self.quantizations = [deepcopy(quantization) for quantization in quantizations] + self.bit_width = self.quantizations[-1].bit_width self.mix_factor = 0.0 self.step_count = 0 self.steps = steps def step(self): - print("before:", self.step_count, self.steps) self.step_count += 1 self.mix_factor = self.step_count / self.steps - print("after:", self.step_count, self.steps) - - assert self.mix_factor <= 1 + self.mix_factor = min(self.mix_factor, 1.0) def quantize(self, x: torch.Tensor): if len(self.quantizations) == 1: @@ -57,22 +56,21 @@ def __init__(self, model, steps, quantizations: List[Quantization], scheduling_p self.scheduled_quantizer = self.get_scheduled_quantizer(scheduling_procedure) self.scheduled_quantizer_instances = [] - self.replace_quantizations(model, exclude_layers, "") - print("got ", len(self.scheduled_quantizer_instances), "quantization schedulers") + self.replace_quantizations(model, exclude_layers) def get_scheduled_quantizer(self, procedure): return self.procedure_classes[procedure] - def replace_quantizations(self, model, exclude_layers, module_name): + def replace_quantizations(self, model, exclude_layers): for name in dir(model): module = getattr(model, name) if issubclass(type(module), Quantization): - print("replaced", module_name, " -> ", name) self.scheduled_quantizer_instances.append(self.scheduled_quantizer(self.quantizations, self.steps)) setattr(model, name, self.scheduled_quantizer_instances[-1]) - for name, children in model.named_children(): - self.replace_quantizations(children, exclude_layers, name) + for child in model.children(): + if type(child) not in exclude_layers: + self.replace_quantizations(child, exclude_layers) def step(self): for scheduled_quantizer in self.scheduled_quantizer_instances: diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 9aca8a9..33255d7 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -150,12 +150,12 @@ def add_optimizer_args(parser: ArgumentParser) -> None: ) optimizer.add_argument( "--scheduled-quantizations", - nargs="*", default=None, + nargs="*", default=["identity", "sign"], help="name of quantizations to schedule", ) optimizer.add_argument( "--quantization-scheduling-procedure", - type=str, default=None, choices=list(Quantization_Scheduler.procedure_classes.keys()), + type=str, default="mix_linear", choices=list(Quantization_Scheduler.procedure_classes.keys()), help="procedure to use for scheduling", ) optimizer.add_argument( diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index eee5efe..08ed093 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -103,7 +103,7 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: return loss - def on_epoch_end(self) -> None: + def on_validation_epoch_end(self) -> None: if self.quantization_scheduler is not None: self.quantization_scheduler.step() self.log("quantization_scheduler/mix_factor", From b12958b5e1d672ef10cce1215b8fe3745ea5f3f2 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 21:14:37 +0200 Subject: [PATCH 149/208] added scheduling test --- tests/models/test_model_conversion.py | 4 +- .../test_quantization_scheduler.py | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/quantizations/test_quantization_scheduler.py diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 55df0b6..2e82b18 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -8,7 +8,6 @@ import bitorch import bitorch.runtime_mode from bitorch import RuntimeMode -from examples.pytorch_lightning.datasets import MNIST from bitorch.layers import QConv2d, QLinear from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin from bitorch.layers.extensions import LayerRecipe @@ -23,11 +22,12 @@ from bitorch.models import Model TEST_MODE = RuntimeMode.INFERENCE_AUTO +MNIST = [(1, 1, 28, 28), 10, "MNIST"] class _TestModel(Model): def __init__(self): - super().__init__(input_shape=MNIST.shape, num_classes=MNIST.num_classes) + super().__init__(input_shape=MNIST[0], num_classes=MNIST[1]) self.q_conv2d = QConv2d(1, 32, 3, 1, 1) self.q_linear = QLinear(784, 64) self._model = nn.Sequential( diff --git a/tests/quantizations/test_quantization_scheduler.py b/tests/quantizations/test_quantization_scheduler.py new file mode 100644 index 0000000..5bc4ee1 --- /dev/null +++ b/tests/quantizations/test_quantization_scheduler.py @@ -0,0 +1,39 @@ +import torch +from bitorch.models import LeNet +from bitorch.quantizations import Quantization_Scheduler, Sign, WeightDoReFa +from bitorch.quantizations.quantization_scheduler import MixLinearScheduling + +INPUT_SHAPE = (10, 1, 28, 28) + + +def test_scheduler(): + torch.manual_seed(123) + model = LeNet(INPUT_SHAPE, 10, 0) + torch.manual_seed(123) + model_unscheduled = LeNet(INPUT_SHAPE, 10, 0) + torch.manual_seed(123) + model_dorefa = LeNet(INPUT_SHAPE, 10, 1) + + par1 = list(model.parameters())[0] + par2 = list(model_unscheduled.parameters())[0] + par3 = list(model_dorefa.parameters())[0] + assert torch.equal(par1, par2) + assert torch.equal(par1, par3) + + scheduler = Quantization_Scheduler(model, 2, [Sign(), WeightDoReFa(), Sign()], scheduling_procedure="mix_linear") + assert scheduler.scheduled_quantizer is MixLinearScheduling + + input_data = torch.rand(INPUT_SHAPE) + sign_output = model_unscheduled(input_data) + dorefa_output = model_dorefa(input_data) + + scheduled_output = model(input_data) + assert torch.equal(scheduled_output, sign_output) + scheduler.step() + + scheduled_output = model(input_data) + assert torch.equal(scheduled_output, dorefa_output) + scheduler.step() + + scheduled_output = model(input_data) + assert torch.equal(scheduled_output, sign_output) From b1dcbf5836af89ba432077092f38e76f4828a730 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 21:44:47 +0200 Subject: [PATCH 150/208] made linters happy --- bitorch/layers/pact.py | 2 +- bitorch/quantizations/__init__.py | 1 + bitorch/quantizations/dorefa.py | 4 +- .../quantizations/quantization_scheduler.py | 107 ++++++++++++++---- .../pytorch_lightning/image_classification.py | 15 ++- .../pytorch_lightning/utils/arg_parser.py | 10 +- examples/pytorch_lightning/utils/callbacks.py | 1 + .../utils/lightning_model.py | 7 +- examples/pytorch_lightning/utils/log.py | 1 + 9 files changed, 114 insertions(+), 34 deletions(-) diff --git a/bitorch/layers/pact.py b/bitorch/layers/pact.py index aa9ee87..62f9bb5 100644 --- a/bitorch/layers/pact.py +++ b/bitorch/layers/pact.py @@ -14,7 +14,7 @@ def forward(ctx, input_tensor: torch.Tensor, alpha: torch.nn.Parameter, bits: in ctx.save_for_backward(input_tensor, alpha) # y_1 = 0.5 * ( torch.abs(x).detach() - torch.abs(x - alpha).detach() + alpha.item() ) clamped = torch.clamp(input_tensor, min=0, max=alpha.item()) - scale = (2**bits - 1) / alpha + scale = (2 ** bits - 1) / alpha quantized = torch.round(clamped * scale) / scale return quantized diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 4cea15c..0c2f184 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -31,6 +31,7 @@ "Sign", "SteHeaviside", "SwishSign", + "Quantization_Scheduler", ] diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index adb1ffd..f9c32ec 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -26,7 +26,7 @@ def __init__(self, bits: Union[int, None] = None) -> None: """ super(WeightDoReFa, self).__init__() self.bit_width = bits or config.dorefa_bits - self._max_value = 2**self.bit_width - 1 + self._max_value = 2 ** self.bit_width - 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """DoReFas the tensor to desired bit resolution using weight dorefa. @@ -59,7 +59,7 @@ def forward( Returns: torch.Tensor: the quantized input tensor """ - max_value = 2**bits - 1 + max_value = 2 ** bits - 1 quantized_tensor = torch.round(torch.clamp(input_tensor, 0, 1) * max_value) / max_value return quantized_tensor diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py index 0dd9bef..ea744a9 100644 --- a/bitorch/quantizations/quantization_scheduler.py +++ b/bitorch/quantizations/quantization_scheduler.py @@ -1,49 +1,105 @@ -import sched from torch.nn import Module import torch from typing import List, Type from .base import Quantization from copy import deepcopy -from .config import config -class MixLinearScheduling(Quantization): - name = "__mixlinarscheduling__" +class ScheduledQuantizer(Quantization): + """Base class for scheduled quantizers to inherit from""" + + name = "__scheduledquantizer__" bit_width = 32 - def __init__(self, quantizations, steps): + def __init__(self, quantizations: List[Quantization], steps: int) -> None: + """Initias scheduled optimizer and sets bitwidth to width of last quantization to be scheduled. + + Args: + quantizations (List[Quantization]): list of quantizations to be scheduled + steps (int): number of steps. at the end of each step, the step() method has to be called once. + """ super().__init__() self.quantizations = [deepcopy(quantization) for quantization in quantizations] self.bit_width = self.quantizations[-1].bit_width - self.mix_factor = 0.0 self.step_count = 0 + self.factor = 0.0 self.steps = steps - def step(self): + def step(self) -> None: + """increments step count and updates internal factor variable""" self.step_count += 1 - self.mix_factor = self.step_count / self.steps - self.mix_factor = min(self.mix_factor, 1.0) + self.factor = self.step_count / self.steps + self.factor = min(self.factor, 1.0) + + +class MixLinearScheduling(ScheduledQuantizer): + name = "__mixlinarscheduling__" + + def quantize(self, x: torch.Tensor) -> torch.Tensor: + """interpolates linearly between the output of the specified quantizations. + + Args: + x (torch.Tensor): input tensor - def quantize(self, x: torch.Tensor): + Returns: + torch.Tensor: quantized output tensor + """ if len(self.quantizations) == 1: return self.quantizations[0](x) - scaled_mix_factor = self.mix_factor * (len(self.quantizations) - 1) + + scaled_mix_factor = self.factor * (len(self.quantizations) - 1) lower_idx = int(scaled_mix_factor) higher_idx = lower_idx + 1 if higher_idx == len(self.quantizations): return self.quantizations[lower_idx](x) inter_unit_mix_factor = scaled_mix_factor - lower_idx - return self.quantizations[higher_idx](x) * inter_unit_mix_factor + self.quantizations[lower_idx](x) * (1.0 - inter_unit_mix_factor) + return self.quantizations[higher_idx](x) * inter_unit_mix_factor + self.quantizations[lower_idx](x) * ( + 1.0 - inter_unit_mix_factor + ) -class Quantization_Scheduler(Module): +class StepScheduling(ScheduledQuantizer): + name = "__stepscheduling__" + + def quantize(self, x: torch.Tensor) -> torch.Tensor: + """interpolates linearly between the output of the specified quantizations. + + Args: + x (torch.Tensor): input tensor - procedure_classes = { - "mix_linear": MixLinearScheduling, - } + Returns: + torch.Tensor: quantized output tensor + """ + quantization_idx = min(int(self.factor * len(self.quantizations)), len(self.quantizations) - 1) + return self.quantizations[quantization_idx](x) - def __init__(self, model, steps, quantizations: List[Quantization], scheduling_procedure: str, exclude_layers: List[Type] = []): + +class Quantization_Scheduler(Module): + + procedure_classes = {"mix_linear": MixLinearScheduling, "step": StepScheduling} + + def __init__( + self, + model: Module, + steps: int, + quantizations: List[Quantization], + scheduling_procedure: str, + exclude_layers: List[Type] = [], + ) -> None: + """Initiates the quantization scheduler and replaces the activation function inside the model with scheduled + quantizers + + Args: + model (Module): model to be scheduled quantized + steps (int): number of steps, e.g. number of epochs. Each step the step() method has to be called once to + update all scheduled quantizers. + quantizations (List[Quantization]): Quantization functions to be scheduled + scheduling_procedure (str): procedure to be used for scheduling. See available subclasses of + ScheduledQuantizer + exclude_layers (List[Type], optional): list of layers types to exclude from replacement with scheduled + quantizers. Defaults to []. + """ super().__init__() assert steps > 0, "steps has to be an integer > 0" @@ -55,13 +111,21 @@ def __init__(self, model, steps, quantizations: List[Quantization], scheduling_p self.scheduled_quantizer = self.get_scheduled_quantizer(scheduling_procedure) - self.scheduled_quantizer_instances = [] + self.scheduled_quantizer_instances: List[ScheduledQuantizer] = [] self.replace_quantizations(model, exclude_layers) - def get_scheduled_quantizer(self, procedure): + def get_scheduled_quantizer(self, procedure: str) -> Type: return self.procedure_classes[procedure] - def replace_quantizations(self, model, exclude_layers): + def replace_quantizations(self, model: Module, exclude_layers: List[Type]) -> None: + """replaces all quantization functions present in the model with a scheduled quantizer. + iterates recursevely to the model layers. + + Args: + model (Module): model have the quantization functions replaced + exclude_layers (List[Type]): list of layers to exclude from replacement, e.g. if QConv2d is specified, + the quantization functions from all QConv2d layers (input and weight) are not replaced + """ for name in dir(model): module = getattr(model, name) if issubclass(type(module), Quantization): @@ -72,6 +136,7 @@ def replace_quantizations(self, model, exclude_layers): if type(child) not in exclude_layers: self.replace_quantizations(child, exclude_layers) - def step(self): + def step(self) -> None: + """updates all instances of scheduled quantizers in the model""" for scheduled_quantizer in self.scheduled_quantizer_instances: scheduled_quantizer.step() diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 31e679b..92cc6d6 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -25,7 +25,7 @@ import bitorch from bitorch import apply_args_to_configuration, RuntimeMode -from bitorch.quantizations import Quantization_Scheduler, Sign, Identity, quantization_from_name +from bitorch.quantizations import Quantization_Scheduler, quantization_from_name from datasets import dataset_from_name from bitorch.models import model_from_name from bitorch.quantizations import Quantization @@ -102,13 +102,18 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - model = model_from_name(args.model)(**model_kwargs, input_shape=dataset.shape, - num_classes=dataset.num_classes) # type: ignore + model = model_from_name(args.model)( + **model_kwargs, input_shape=dataset.shape, num_classes=dataset.num_classes + ) # type: ignore model.initialize() if args.quantization_scheduling: - quantization_scheduler = Quantization_Scheduler(model, quantizations=[quantization_from_name( - name)() for name in args.scheduled_quantizations], scheduling_procedure=args.quantization_scheduling_procedure, steps=args.max_epochs) + quantization_scheduler = Quantization_Scheduler( + model, + quantizations=[quantization_from_name(name)() for name in args.scheduled_quantizations], + scheduling_procedure=args.quantization_scheduling_procedure, + steps=args.max_epochs, + ) else: quantization_scheduler = None diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 33255d7..3559062 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -145,17 +145,21 @@ def add_optimizer_args(parser: ArgumentParser) -> None: ) optimizer.add_argument( "--quantization-scheduling", - action="store_true", default=False, + action="store_true", + default=False, help="toggles weather to use quantization scheduling", ) optimizer.add_argument( "--scheduled-quantizations", - nargs="*", default=["identity", "sign"], + nargs="*", + default=["identity", "sign"], help="name of quantizations to schedule", ) optimizer.add_argument( "--quantization-scheduling-procedure", - type=str, default="mix_linear", choices=list(Quantization_Scheduler.procedure_classes.keys()), + type=str, + default="mix_linear", + choices=list(Quantization_Scheduler.procedure_classes.keys()), help="procedure to use for scheduling", ) optimizer.add_argument( diff --git a/examples/pytorch_lightning/utils/callbacks.py b/examples/pytorch_lightning/utils/callbacks.py index f2be7a3..bdbdb3f 100644 --- a/examples/pytorch_lightning/utils/callbacks.py +++ b/examples/pytorch_lightning/utils/callbacks.py @@ -1,3 +1,4 @@ +# type: ignore import pytorch_lightning as pl from bitorch.quantizations import ProgressiveSign diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 08ed093..14f16aa 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -1,3 +1,4 @@ +# type: ignore import logging from argparse import Namespace from typing import Union, Any @@ -106,8 +107,10 @@ def validation_step(self, batch: torch.Tensor, batch_idx: int) -> None: # type: def on_validation_epoch_end(self) -> None: if self.quantization_scheduler is not None: self.quantization_scheduler.step() - self.log("quantization_scheduler/mix_factor", - self.quantization_scheduler.scheduled_quantizer_instances[0].mix_factor) + self.log( + "quantization_scheduler/mix_factor", + self.quantization_scheduler.scheduled_quantizer_instances[0].mix_factor, + ) return super().on_epoch_end() def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: ignore diff --git a/examples/pytorch_lightning/utils/log.py b/examples/pytorch_lightning/utils/log.py index ea49921..a26848d 100644 --- a/examples/pytorch_lightning/utils/log.py +++ b/examples/pytorch_lightning/utils/log.py @@ -1,3 +1,4 @@ +# type: ignore import logging import time from typing import Optional, Any, Dict, List, Union From 01d9592359fb653af79b008ce698cba1ed4a951a Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 8 Sep 2022 21:45:41 +0200 Subject: [PATCH 151/208] edited changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d09a27..35f6274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - improved logging - callback to update Progressive Sign module - option to integrate custom models, datasets, quantization functions +- a quantization scheduler which lets you change quantization methods during training ### Changed From 66e4a32129e1d3005bb58f6daf59ccd3d494eaec Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 9 Sep 2022 12:23:04 +0200 Subject: [PATCH 152/208] added scheduledquantization class and partial replacement --- bitorch/quantizations/__init__.py | 3 +- .../quantizations/quantization_scheduler.py | 65 ++++++++++++++++--- .../pytorch_lightning/image_classification.py | 1 + .../pytorch_lightning/utils/arg_parser.py | 7 ++ .../utils/lightning_model.py | 4 +- .../test_quantization_scheduler.py | 2 +- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 0c2f184..fa9d48a 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -15,7 +15,7 @@ from .ste_heaviside import SteHeaviside from .swish_sign import SwishSign from .progressive_sign import ProgressiveSign -from .quantization_scheduler import Quantization_Scheduler +from .quantization_scheduler import Quantization_Scheduler, ScheduledQuantizer from ..util import build_lookup_dictionary __all__ = [ @@ -32,6 +32,7 @@ "SteHeaviside", "SwishSign", "Quantization_Scheduler", + "ScheduledQuantizer", ] diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py index ea744a9..76c312a 100644 --- a/bitorch/quantizations/quantization_scheduler.py +++ b/bitorch/quantizations/quantization_scheduler.py @@ -1,3 +1,9 @@ +""" +Implementation of a quantization scheduler which replaces quantization functions inside a given model during +training. This module also contains various scheduling procedure implementations which can be extended in future +versions +""" + from torch.nn import Module import torch from typing import List, Type @@ -6,12 +12,31 @@ class ScheduledQuantizer(Quantization): - """Base class for scheduled quantizers to inherit from""" - - name = "__scheduledquantizer__" + """Base class for scheduled quantizers to inherit from. You can also use this quantization method + to indicate to the quantization scheduler that only this quantization should be scheduled. + + e.g. + ``` + model = Sequential( + QConv2d(3, 64, input_quantization="scheduled_quantizer", weight_quantization="sign"), + ReLU(), + flatten(), + QLinear(1000, 10, input_quantization="sign", weight_quantization="sign"), + Softmax(), + ) + # this replaces all quantizations in the model with scheduled quantizers and schedules them during training + scheduler = Quantization_Scheduler(model, [Identity(), InputDorefa()], replace_all_quantizations=True) + + # this only replaces the one instance of the ScheduledQuantizer and leaves the rest unchanged + scheduler = Quantization_Scheduler(model, [Identity(), InputDorefa()], replace_all_quantizations=False) + ``` + + """ + + name = "scheduled_quantizer" bit_width = 32 - def __init__(self, quantizations: List[Quantization], steps: int) -> None: + def __init__(self, quantizations: List[Quantization] = None, steps: int = None) -> None: """Initias scheduled optimizer and sets bitwidth to width of last quantization to be scheduled. Args: @@ -19,8 +44,11 @@ def __init__(self, quantizations: List[Quantization], steps: int) -> None: steps (int): number of steps. at the end of each step, the step() method has to be called once. """ super().__init__() - self.quantizations = [deepcopy(quantization) for quantization in quantizations] - self.bit_width = self.quantizations[-1].bit_width + if quantizations is None: + self.quantizations = None + else: + self.quantizations = [deepcopy(quantization) for quantization in quantizations] + self.bit_width = self.quantizations[-1].bit_width if hasattr(self.quantizations[-1], "bit_width") else 32 self.step_count = 0 self.factor = 0.0 self.steps = steps @@ -85,6 +113,7 @@ def __init__( steps: int, quantizations: List[Quantization], scheduling_procedure: str, + schedule_all_quantizations: bool = False, exclude_layers: List[Type] = [], ) -> None: """Initiates the quantization scheduler and replaces the activation function inside the model with scheduled @@ -97,6 +126,9 @@ def __init__( quantizations (List[Quantization]): Quantization functions to be scheduled scheduling_procedure (str): procedure to be used for scheduling. See available subclasses of ScheduledQuantizer + schedule_all_quantizations (bool): toggles weather all quantizations in the model shall be replaced with + quantized schedulers or weather only the quantized scheduler layers already present shall be used for + scheduling. Defaults to False. exclude_layers (List[Type], optional): list of layers types to exclude from replacement with scheduled quantizers. Defaults to []. """ @@ -112,12 +144,20 @@ def __init__( self.scheduled_quantizer = self.get_scheduled_quantizer(scheduling_procedure) self.scheduled_quantizer_instances: List[ScheduledQuantizer] = [] - self.replace_quantizations(model, exclude_layers) + self.replace_quantizations(model, exclude_layers, schedule_all_quantizations) def get_scheduled_quantizer(self, procedure: str) -> Type: + """gets the scheduling class associated with the given scheduling procedure + + Args: + procedure (str): name of the scheduling procedure to be used + + Returns: + Type: a subclass of ScheduledQuantizer + """ return self.procedure_classes[procedure] - def replace_quantizations(self, model: Module, exclude_layers: List[Type]) -> None: + def replace_quantizations(self, model: Module, exclude_layers: List[Type], replace_all_quantizations: bool) -> None: """replaces all quantization functions present in the model with a scheduled quantizer. iterates recursevely to the model layers. @@ -125,16 +165,21 @@ def replace_quantizations(self, model: Module, exclude_layers: List[Type]) -> No model (Module): model have the quantization functions replaced exclude_layers (List[Type]): list of layers to exclude from replacement, e.g. if QConv2d is specified, the quantization functions from all QConv2d layers (input and weight) are not replaced + replace_all_quantizations (bool): toggles weather to replace all quantizations or just the instances + of ScheduledQuantizer """ for name in dir(model): module = getattr(model, name) - if issubclass(type(module), Quantization): + if replace_all_quantizations and issubclass(type(module), Quantization): + self.scheduled_quantizer_instances.append(self.scheduled_quantizer(self.quantizations, self.steps)) + setattr(model, name, self.scheduled_quantizer_instances[-1]) + elif not replace_all_quantizations and issubclass(type(module), ScheduledQuantizer): self.scheduled_quantizer_instances.append(self.scheduled_quantizer(self.quantizations, self.steps)) setattr(model, name, self.scheduled_quantizer_instances[-1]) for child in model.children(): if type(child) not in exclude_layers: - self.replace_quantizations(child, exclude_layers) + self.replace_quantizations(child, exclude_layers, replace_all_quantizations) def step(self) -> None: """updates all instances of scheduled quantizers in the model""" diff --git a/examples/pytorch_lightning/image_classification.py b/examples/pytorch_lightning/image_classification.py index 92cc6d6..af0113c 100644 --- a/examples/pytorch_lightning/image_classification.py +++ b/examples/pytorch_lightning/image_classification.py @@ -112,6 +112,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model, quantizations=[quantization_from_name(name)() for name in args.scheduled_quantizations], scheduling_procedure=args.quantization_scheduling_procedure, + schedule_all_quantizations=args.schedule_all_quantizations, steps=args.max_epochs, ) else: diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index 3559062..51c37aa 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -149,6 +149,13 @@ def add_optimizer_args(parser: ArgumentParser) -> None: default=False, help="toggles weather to use quantization scheduling", ) + optimizer.add_argument( + "--schedule-all-quantizations", + action="store_true", + default=False, + help="toggles weather to replace all quantizations inside the model with scheduled quantizers or " + "to just use the instances of ScheduledQuantizer which are already present in the model.", + ) optimizer.add_argument( "--scheduled-quantizations", nargs="*", diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/pytorch_lightning/utils/lightning_model.py index 14f16aa..4dbb0d8 100644 --- a/examples/pytorch_lightning/utils/lightning_model.py +++ b/examples/pytorch_lightning/utils/lightning_model.py @@ -108,8 +108,8 @@ def on_validation_epoch_end(self) -> None: if self.quantization_scheduler is not None: self.quantization_scheduler.step() self.log( - "quantization_scheduler/mix_factor", - self.quantization_scheduler.scheduled_quantizer_instances[0].mix_factor, + "quantization_scheduler/factor", + self.quantization_scheduler.scheduled_quantizer_instances[0].factor, ) return super().on_epoch_end() diff --git a/tests/quantizations/test_quantization_scheduler.py b/tests/quantizations/test_quantization_scheduler.py index 5bc4ee1..827c725 100644 --- a/tests/quantizations/test_quantization_scheduler.py +++ b/tests/quantizations/test_quantization_scheduler.py @@ -18,7 +18,7 @@ def test_scheduler(): par2 = list(model_unscheduled.parameters())[0] par3 = list(model_dorefa.parameters())[0] assert torch.equal(par1, par2) - assert torch.equal(par1, par3) + assert torch.equal(par2, par3) scheduler = Quantization_Scheduler(model, 2, [Sign(), WeightDoReFa(), Sign()], scheduling_procedure="mix_linear") assert scheduler.scheduled_quantizer is MixLinearScheduling From 65f0df0c1a0268393b6a0ac1e6171a142f72a98f Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 9 Sep 2022 12:27:14 +0200 Subject: [PATCH 153/208] made linters happy --- bitorch/layers/pact.py | 2 +- bitorch/quantizations/dorefa.py | 4 ++-- bitorch/quantizations/quantization_scheduler.py | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/bitorch/layers/pact.py b/bitorch/layers/pact.py index 62f9bb5..aa9ee87 100644 --- a/bitorch/layers/pact.py +++ b/bitorch/layers/pact.py @@ -14,7 +14,7 @@ def forward(ctx, input_tensor: torch.Tensor, alpha: torch.nn.Parameter, bits: in ctx.save_for_backward(input_tensor, alpha) # y_1 = 0.5 * ( torch.abs(x).detach() - torch.abs(x - alpha).detach() + alpha.item() ) clamped = torch.clamp(input_tensor, min=0, max=alpha.item()) - scale = (2 ** bits - 1) / alpha + scale = (2**bits - 1) / alpha quantized = torch.round(clamped * scale) / scale return quantized diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index f9c32ec..adb1ffd 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -26,7 +26,7 @@ def __init__(self, bits: Union[int, None] = None) -> None: """ super(WeightDoReFa, self).__init__() self.bit_width = bits or config.dorefa_bits - self._max_value = 2 ** self.bit_width - 1 + self._max_value = 2**self.bit_width - 1 def quantize(self, x: torch.Tensor) -> torch.Tensor: """DoReFas the tensor to desired bit resolution using weight dorefa. @@ -59,7 +59,7 @@ def forward( Returns: torch.Tensor: the quantized input tensor """ - max_value = 2 ** bits - 1 + max_value = 2**bits - 1 quantized_tensor = torch.round(torch.clamp(input_tensor, 0, 1) * max_value) / max_value return quantized_tensor diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py index 76c312a..0d8666c 100644 --- a/bitorch/quantizations/quantization_scheduler.py +++ b/bitorch/quantizations/quantization_scheduler.py @@ -36,7 +36,7 @@ class ScheduledQuantizer(Quantization): name = "scheduled_quantizer" bit_width = 32 - def __init__(self, quantizations: List[Quantization] = None, steps: int = None) -> None: + def __init__(self, quantizations: List[Quantization] = [], steps: int = 0) -> None: """Initias scheduled optimizer and sets bitwidth to width of last quantization to be scheduled. Args: @@ -44,10 +44,8 @@ def __init__(self, quantizations: List[Quantization] = None, steps: int = None) steps (int): number of steps. at the end of each step, the step() method has to be called once. """ super().__init__() - if quantizations is None: - self.quantizations = None - else: - self.quantizations = [deepcopy(quantization) for quantization in quantizations] + self.quantizations = [deepcopy(quantization) for quantization in quantizations] + if len(quantizations) > 0: self.bit_width = self.quantizations[-1].bit_width if hasattr(self.quantizations[-1], "bit_width") else 32 self.step_count = 0 self.factor = 0.0 From 39fc1d238ae622082bb1d35cbcd0b41efc596ea1 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 9 Sep 2022 13:18:28 +0200 Subject: [PATCH 154/208] fixed tests --- bitorch/quantizations/__init__.py | 2 +- bitorch/quantizations/quantization_scheduler.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index fa9d48a..23ef8d2 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -36,7 +36,7 @@ ] -quantizations_by_name: Dict[str, Type[Quantization]] = build_lookup_dictionary(__name__, __all__, Quantization) +quantizations_by_name: Dict[str, Type[Quantization]] = build_lookup_dictionary(__name__, __all__[:-2], Quantization) def quantization_from_name(name: str) -> Type[Quantization]: diff --git a/bitorch/quantizations/quantization_scheduler.py b/bitorch/quantizations/quantization_scheduler.py index 0d8666c..6c1ddfc 100644 --- a/bitorch/quantizations/quantization_scheduler.py +++ b/bitorch/quantizations/quantization_scheduler.py @@ -57,6 +57,17 @@ def step(self) -> None: self.factor = self.step_count / self.steps self.factor = min(self.factor, 1.0) + def quantize(self, x: torch.Tensor) -> torch.Tensor: + """dummy quantization function for compability reasons. + + Args: + x (torch.Tensor): input tensor + + Returns: + torch.Tensor: unchanged input tensor + """ + return x + class MixLinearScheduling(ScheduledQuantizer): name = "__mixlinarscheduling__" From cf85cf7d3340446aba8157b67cbb46cec298a75f Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 9 Sep 2022 16:03:36 +0200 Subject: [PATCH 155/208] removed unnecessary fix --- bitorch/quantizations/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/quantizations/__init__.py b/bitorch/quantizations/__init__.py index 23ef8d2..fa9d48a 100644 --- a/bitorch/quantizations/__init__.py +++ b/bitorch/quantizations/__init__.py @@ -36,7 +36,7 @@ ] -quantizations_by_name: Dict[str, Type[Quantization]] = build_lookup_dictionary(__name__, __all__[:-2], Quantization) +quantizations_by_name: Dict[str, Type[Quantization]] = build_lookup_dictionary(__name__, __all__, Quantization) def quantization_from_name(name: str) -> Type[Quantization]: From d8fd73634d27de5e1f1b57c7fc00c47cec830984 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 21 Sep 2022 12:31:04 +0200 Subject: [PATCH 156/208] fix relative imports --- examples/pytorch_lightning/datasets/base.py | 2 +- examples/pytorch_lightning/utils/arg_parser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/pytorch_lightning/datasets/base.py b/examples/pytorch_lightning/datasets/base.py index c48bf18..f006b59 100644 --- a/examples/pytorch_lightning/datasets/base.py +++ b/examples/pytorch_lightning/datasets/base.py @@ -7,7 +7,7 @@ from torch.utils.data import Dataset from torchvision.transforms import transforms -from ..datasets.dummy_dataset import DummyDataset +from .dummy_dataset import DummyDataset class BasicDataset(Dataset): diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/pytorch_lightning/utils/arg_parser.py index a201c5c..1fcc5e4 100644 --- a/examples/pytorch_lightning/utils/arg_parser.py +++ b/examples/pytorch_lightning/utils/arg_parser.py @@ -8,8 +8,8 @@ import bitorch from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin -from ..datasets import dataset_names -from ..utils.teachers import available_teachers +from datasets import dataset_names +from .teachers import available_teachers class _HeadArgumentParser(ArgumentParser): From 075e97543eddd81b9b645d57d1788338ffcdda63 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 16 Sep 2022 15:14:18 +0200 Subject: [PATCH 157/208] renamed examples dir --- examples/{pytorch_lightning => image_classification}/.gitignore | 0 examples/{pytorch_lightning => image_classification}/README.md | 0 examples/{pytorch_lightning => image_classification}/__init__.py | 0 .../datasets/__init__.py | 0 .../{pytorch_lightning => image_classification}/datasets/base.py | 0 .../{pytorch_lightning => image_classification}/datasets/cifar.py | 0 .../datasets/dummy_dataset.py | 0 .../datasets/imagenet.py | 0 .../{pytorch_lightning => image_classification}/datasets/mnist.py | 0 .../image_classification.py | 0 .../{pytorch_lightning => image_classification}/requirements.txt | 0 .../{pytorch_lightning => image_classification}/utils/__init__.py | 0 .../utils/arg_parser.py | 0 .../utils/callbacks.py | 0 .../{pytorch_lightning => image_classification}/utils/kd_loss.py | 0 .../utils/lightning_model.py | 0 examples/{pytorch_lightning => image_classification}/utils/log.py | 0 .../{pytorch_lightning => image_classification}/utils/teachers.py | 0 .../utils/unused_args.py | 0 .../{pytorch_lightning => image_classification}/utils/utils.py | 0 .../utils/wandb_logger.py | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename examples/{pytorch_lightning => image_classification}/.gitignore (100%) rename examples/{pytorch_lightning => image_classification}/README.md (100%) rename examples/{pytorch_lightning => image_classification}/__init__.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/__init__.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/base.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/cifar.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/dummy_dataset.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/imagenet.py (100%) rename examples/{pytorch_lightning => image_classification}/datasets/mnist.py (100%) rename examples/{pytorch_lightning => image_classification}/image_classification.py (100%) rename examples/{pytorch_lightning => image_classification}/requirements.txt (100%) rename examples/{pytorch_lightning => image_classification}/utils/__init__.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/arg_parser.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/callbacks.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/kd_loss.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/lightning_model.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/log.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/teachers.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/unused_args.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/utils.py (100%) rename examples/{pytorch_lightning => image_classification}/utils/wandb_logger.py (100%) diff --git a/examples/pytorch_lightning/.gitignore b/examples/image_classification/.gitignore similarity index 100% rename from examples/pytorch_lightning/.gitignore rename to examples/image_classification/.gitignore diff --git a/examples/pytorch_lightning/README.md b/examples/image_classification/README.md similarity index 100% rename from examples/pytorch_lightning/README.md rename to examples/image_classification/README.md diff --git a/examples/pytorch_lightning/__init__.py b/examples/image_classification/__init__.py similarity index 100% rename from examples/pytorch_lightning/__init__.py rename to examples/image_classification/__init__.py diff --git a/examples/pytorch_lightning/datasets/__init__.py b/examples/image_classification/datasets/__init__.py similarity index 100% rename from examples/pytorch_lightning/datasets/__init__.py rename to examples/image_classification/datasets/__init__.py diff --git a/examples/pytorch_lightning/datasets/base.py b/examples/image_classification/datasets/base.py similarity index 100% rename from examples/pytorch_lightning/datasets/base.py rename to examples/image_classification/datasets/base.py diff --git a/examples/pytorch_lightning/datasets/cifar.py b/examples/image_classification/datasets/cifar.py similarity index 100% rename from examples/pytorch_lightning/datasets/cifar.py rename to examples/image_classification/datasets/cifar.py diff --git a/examples/pytorch_lightning/datasets/dummy_dataset.py b/examples/image_classification/datasets/dummy_dataset.py similarity index 100% rename from examples/pytorch_lightning/datasets/dummy_dataset.py rename to examples/image_classification/datasets/dummy_dataset.py diff --git a/examples/pytorch_lightning/datasets/imagenet.py b/examples/image_classification/datasets/imagenet.py similarity index 100% rename from examples/pytorch_lightning/datasets/imagenet.py rename to examples/image_classification/datasets/imagenet.py diff --git a/examples/pytorch_lightning/datasets/mnist.py b/examples/image_classification/datasets/mnist.py similarity index 100% rename from examples/pytorch_lightning/datasets/mnist.py rename to examples/image_classification/datasets/mnist.py diff --git a/examples/pytorch_lightning/image_classification.py b/examples/image_classification/image_classification.py similarity index 100% rename from examples/pytorch_lightning/image_classification.py rename to examples/image_classification/image_classification.py diff --git a/examples/pytorch_lightning/requirements.txt b/examples/image_classification/requirements.txt similarity index 100% rename from examples/pytorch_lightning/requirements.txt rename to examples/image_classification/requirements.txt diff --git a/examples/pytorch_lightning/utils/__init__.py b/examples/image_classification/utils/__init__.py similarity index 100% rename from examples/pytorch_lightning/utils/__init__.py rename to examples/image_classification/utils/__init__.py diff --git a/examples/pytorch_lightning/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py similarity index 100% rename from examples/pytorch_lightning/utils/arg_parser.py rename to examples/image_classification/utils/arg_parser.py diff --git a/examples/pytorch_lightning/utils/callbacks.py b/examples/image_classification/utils/callbacks.py similarity index 100% rename from examples/pytorch_lightning/utils/callbacks.py rename to examples/image_classification/utils/callbacks.py diff --git a/examples/pytorch_lightning/utils/kd_loss.py b/examples/image_classification/utils/kd_loss.py similarity index 100% rename from examples/pytorch_lightning/utils/kd_loss.py rename to examples/image_classification/utils/kd_loss.py diff --git a/examples/pytorch_lightning/utils/lightning_model.py b/examples/image_classification/utils/lightning_model.py similarity index 100% rename from examples/pytorch_lightning/utils/lightning_model.py rename to examples/image_classification/utils/lightning_model.py diff --git a/examples/pytorch_lightning/utils/log.py b/examples/image_classification/utils/log.py similarity index 100% rename from examples/pytorch_lightning/utils/log.py rename to examples/image_classification/utils/log.py diff --git a/examples/pytorch_lightning/utils/teachers.py b/examples/image_classification/utils/teachers.py similarity index 100% rename from examples/pytorch_lightning/utils/teachers.py rename to examples/image_classification/utils/teachers.py diff --git a/examples/pytorch_lightning/utils/unused_args.py b/examples/image_classification/utils/unused_args.py similarity index 100% rename from examples/pytorch_lightning/utils/unused_args.py rename to examples/image_classification/utils/unused_args.py diff --git a/examples/pytorch_lightning/utils/utils.py b/examples/image_classification/utils/utils.py similarity index 100% rename from examples/pytorch_lightning/utils/utils.py rename to examples/image_classification/utils/utils.py diff --git a/examples/pytorch_lightning/utils/wandb_logger.py b/examples/image_classification/utils/wandb_logger.py similarity index 100% rename from examples/pytorch_lightning/utils/wandb_logger.py rename to examples/image_classification/utils/wandb_logger.py From 9210bb0dd66ba754d4250b1894ccd360b8b3b4db Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 16 Sep 2022 15:21:15 +0200 Subject: [PATCH 158/208] changed name to image_classification --- tests/models/test_model_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models/test_model_conversion.py b/tests/models/test_model_conversion.py index 55df0b6..1935705 100644 --- a/tests/models/test_model_conversion.py +++ b/tests/models/test_model_conversion.py @@ -8,7 +8,7 @@ import bitorch import bitorch.runtime_mode from bitorch import RuntimeMode -from examples.pytorch_lightning.datasets import MNIST +from examples.image_classification.datasets import MNIST from bitorch.layers import QConv2d, QLinear from bitorch.layers.extensions.layer_implementation import CustomImplementationMixin from bitorch.layers.extensions import LayerRecipe From 8364274d9d3411a76793221a91d571d9adf8b780 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 16 Sep 2022 16:54:04 +0200 Subject: [PATCH 159/208] fixed setup --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d67aaaf..e23c5b3 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,8 @@ def _get_requirements(*file_path: Union[Path, str]): requirements_list = [] for fp in file_path: - requirements_list.extend(list(requirement.strip() for requirement in (root_path / fp).open().readlines())) + with (root_path / fp).open() as requirements_file: + requirements_list.extend(list(requirement.strip() for requirement in requirements_file.readlines())) # exclude bitorch from examples if "bitorch" in requirements_list: requirements_list.remove("bitorch") @@ -47,7 +48,7 @@ def _get_files_recursively(glob: str, root: str = ".") -> List[str]: extras_require={ "dev": _get_requirements("requirements-dev.txt"), # "opt": _get_requirements(*_get_files_recursively("requirements*.txt", root="examples")), - "opt": _get_requirements("examples/pytorch_lightning/requirements.txt"), + "opt": _get_requirements("examples/image_classification/requirements.txt", "examples/mnist/requirements.txt"), }, classifiers=[ "Development Status :: 3 - Alpha", From b04c920a63eee5ff957753f3d87476880e596ab8 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 19 Oct 2022 15:41:15 +0200 Subject: [PATCH 160/208] fix paths --- examples/image_classification/image_classification.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index 13df977..7ef88d2 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -28,9 +28,9 @@ from datasets import dataset_from_name from bitorch.models import model_from_name from bitorch.quantizations import Quantization -from examples.pytorch_lightning.utils.callbacks import ProgressiveSignScalerCallback -from examples.pytorch_lightning.utils.log import CommandLineLogger -from examples.pytorch_lightning.utils.wandb_logger import CustomWandbLogger +from examples.image_classification.utils.callbacks import ProgressiveSignScalerCallback +from examples.image_classification.utils.log import CommandLineLogger +from examples.image_classification.utils.wandb_logger import CustomWandbLogger from utils.arg_parser import create_argparser from utils.lightning_model import ModelWrapper, DistillationModelWrapper from utils.utils import configure_logging From 3dc1b408b0fa5df72bea670308d53df13043a047 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 21 Oct 2022 15:44:44 +0200 Subject: [PATCH 161/208] tried older flake version --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7f54ff4..4a6df5e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ black build -flake8 +flake8==3.8.2 flake8-docstrings mypy~=0.920 myst-nb From ca1b026c18626edcac2ce7f09d499057c01cb4e1 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 21 Oct 2022 15:56:30 +0200 Subject: [PATCH 162/208] changed importlib metadata version --- requirements-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4a6df5e..bd41a2c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ black build -flake8==3.8.2 +importlib-metadata==4.13.0 +flake8 flake8-docstrings mypy~=0.920 myst-nb From 358790ec542052989d73a746296b6fcd50509a7c Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Wed, 26 Oct 2022 15:06:13 +0200 Subject: [PATCH 163/208] add explicit importlib version --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7f54ff4..3fa5245 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ black build flake8 flake8-docstrings +importlib_metadata<5 mypy~=0.920 myst-nb nbclient==0.5.13 From cea5596478c1a25cffc8d77726d0dab8c4cc105c Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 27 Oct 2022 15:55:49 +0200 Subject: [PATCH 164/208] fixed bug in ste heaviside implementation --- bitorch/quantizations/ste_heaviside.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bitorch/quantizations/ste_heaviside.py b/bitorch/quantizations/ste_heaviside.py index bf9195c..4c3da4a 100644 --- a/bitorch/quantizations/ste_heaviside.py +++ b/bitorch/quantizations/ste_heaviside.py @@ -1,6 +1,7 @@ """Sign Function Implementation""" import torch import typing +from typing import Any from .base import STE, Quantization @@ -20,6 +21,8 @@ def forward( Returns: torch.Tensor: the quantized input tensor """ + ctx.save_for_backward(input_tensor) + quantized_tensor = torch.where( input_tensor > 0, torch.tensor(1.0, device=input_tensor.device), @@ -27,6 +30,23 @@ def forward( ) return quantized_tensor + @staticmethod + @typing.no_type_check + def backward(ctx: Any, output_gradient: torch.Tensor) -> torch.Tensor: + """just passes the unchanged output gradient as input gradient. + + Args: + ctx (Any): autograd context + output_gradient (torch.Tensor): output gradient + + Returns: + torch.Tensor: the unchanged output gradient + """ + input_tensor = ctx.saved_tensors[0] + inside_threshold = torch.abs(input_tensor) <= 1 + print("over threshold:", len(input_tensor) - torch.sum(inside_threshold)) + return output_gradient * inside_threshold + class SteHeaviside(Quantization): """Module for applying the SteHeaviside quantization, using an ste in backward pass""" From 689b1c9ad87bfdee590cf1a6e449d508718cbac4 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Thu, 27 Oct 2022 16:24:18 +0200 Subject: [PATCH 165/208] fixed bug in ste heaviside implementation --- bitorch/quantizations/ste_heaviside.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/quantizations/ste_heaviside.py b/bitorch/quantizations/ste_heaviside.py index 4c3da4a..d490878 100644 --- a/bitorch/quantizations/ste_heaviside.py +++ b/bitorch/quantizations/ste_heaviside.py @@ -26,7 +26,7 @@ def forward( quantized_tensor = torch.where( input_tensor > 0, torch.tensor(1.0, device=input_tensor.device), - torch.tensor(0.0, device=input_tensor.device), + torch.tensor(-1.0, device=input_tensor.device), ) return quantized_tensor From c02013bf5933fb27e05c433c8d5e661492015895 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 28 Oct 2022 00:00:22 +0200 Subject: [PATCH 166/208] fixed weightdorefa and added basic visualization notebook --- bitorch/quantizations/dorefa.py | 45 ++++- bitorch/quantizations/progressive_sign.py | 6 +- .../notebooks/Quantization_Visualiztion.ipynb | 184 ++++++++++++++++++ examples/notebooks/quantization_functions.png | Bin 0 -> 1264988 bytes 4 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 examples/notebooks/Quantization_Visualiztion.ipynb create mode 100644 examples/notebooks/quantization_functions.png diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index adb1ffd..cd2b6a2 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -8,6 +8,45 @@ from .config import config +class WeightDoReFaFunction(Function): + @staticmethod + @typing.no_type_check + def forward( + ctx: torch.autograd.function.BackwardCFunction, + input_tensor: torch.Tensor, + maximum_bit_value: int) -> torch.Tensor: + """quantizes input tensor and forwards it. + + Args: + ctx (Any): autograd context + input_tensor (torch.Tensor): input tensor + bits (int): number of bits to round the input tensor to + + Returns: + torch.Tensor: the quantized input tensor + """ + ctx.save_for_backward(input_tensor) + + squashed_values = torch.tanh(input_tensor) + max_val = torch.max(torch.abs(squashed_values)).detach() + adjusted_values = squashed_values / (2.0 * max_val) + 0.5 + return 2.0 * (torch.round(adjusted_values * maximum_bit_value) / maximum_bit_value) - 1.0 + + @staticmethod + @typing.no_type_check + def backward(ctx: Any, output_gradient: torch.Tensor) -> torch.Tensor: + """just passes the unchanged output gradient as input gradient. + + Args: + ctx (Any): autograd context + output_gradient (torch.Tensor): output gradient + + Returns: + torch.Tensor: the unchanged output gradient + """ + return output_gradient, None, None + + class WeightDoReFa(Quantization): """Module for applying the dorefa function on weights. @@ -37,10 +76,8 @@ def quantize(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: DoReFaed tensor x """ - squashed_values = torch.tanh(x) - max_val = torch.max(torch.abs(squashed_values)).detach() - adjusted_values = squashed_values / (2.0 * max_val) + 0.5 - return 2.0 * (torch.round(adjusted_values * self._max_value) / self._max_value) - 1.0 + + return WeightDoReFaFunction.apply(x, self._max_value) class InputDoReFaFunction(Function): diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index bdeb0ce..c293dd9 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -4,9 +4,10 @@ import torch import torch.nn.functional as F +from torch.autograd.function import Function from bitorch.config import Config -from .base import Quantization, STE +from .base import Quantization from .sign import SignFunction EPSILON = 1e-7 @@ -28,7 +29,7 @@ class ProgressiveSignConfig(Config): config = ProgressiveSignConfig() -class ProgressiveSignFunctionTrain(STE): +class ProgressiveSignFunctionTrain(Function): @staticmethod @typing.no_type_check def forward( @@ -46,6 +47,7 @@ def forward( Returns: torch.Tensor: the sign tensor """ + ctx.save_for_backward(input_tensor) # avoid division by zero with EPSILON return F.hardtanh(input_tensor / max(1.0 - temperature, EPSILON)) diff --git a/examples/notebooks/Quantization_Visualiztion.ipynb b/examples/notebooks/Quantization_Visualiztion.ipynb new file mode 100644 index 0000000..53741fb --- /dev/null +++ b/examples/notebooks/Quantization_Visualiztion.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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yqJ+/foMGDDA4/MWFxeTlpYGVL6wPWi5ELvTq2YhJYWFzLvsMrL276dBp04kzpxJiBvbNPuTEl7Ed4qys/n+jjsAOOv++0mowcW7OsphEes69uuvlOTlERYTQ6MzzzzlcY34Eil1usLXgcWLWXTvvQAMe+45Oo8fb0ZoIlIDRfn5/LfCDZ/Lvv2W2CqKUC55eXksX74cgBEjRlS67l9VkpOTMQyDevXqERsbW+lxugbbm141izAMgwV33smhFSuIqF+fCfPnE9WokdlhncKV8CLifcsef5zsAweo3749Q555xifn0DBtEetKXbsWgGZ9+xJ8mrvcyl+RUicvbn981y7mXXEFhsNB12uvZcCf/2xmeCLioXfatcNZVATAOU8/TftRo9x63tKlSyksLKR58+b07NmzRueuOM2xquurPgfbmwpfFrHmpZfYOm0aQcHBJM6cSeMuXcwO6bQ0WkTEN35bvrx8esaFb79NeEyMT86jHBaxLlfh63TTHEH5K+JSccRXQUYGsxMTKTh+nISBAxn97rsqDovYyMdDhpBfNtWwwyWXcM7//Z9bzzt+/DirV68GYOTIkTXOe3fW9wJdg+1Or5oF7P7qK3589FEAhr/8Mu0uvNDkiCqnIZ4i3leUnc03N9wAhkGPm2/26S6uumiLWFfqmjVA5YUvjfgSKeUqfIUGBfHlpEkc+/VXYlu1YvycOYRGRpocnYi4a9H993P4p58AiGvXjsu+/NLt5y5evBin00nHjh3p2LFjjWNwFb6qWt8LdA22O++umiweS9+6la+uvhoMg163307f++4zO6QqaWFdEe9b8tBDZO7dS1zbtgx/5RWfnkuL24tYU3UL24PyV8TFVfg6+MYbHPruO0Kjoxk/bx4xzZubHJmIuGv7jBms/89/AAiNiuLWpCS3n3v48GG2bNkClI72qqmCggKOHj0KuD/iS9dge9ItfxPlpaczOzGRouxsWp9/Phe89prlE0kjvkS8a/f8+Wx+910ICmLMhx8SERfn0/Mph0WsqbqF7UH5K+JSUlLCAODQjBkAXPzRR8T37WtuUCLitqPbt5cO/gAIDubmHTvc2sERytbGXrAAgF69etG8FgVv12ivBg0aEB0dXeWxugbbm141kziKiph3+eVk7t1Lg44dGfv555bbwfF0NMRTxHty09L47rbbAOj/8MO0Pu88n59T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gQM4HA42bNgAQKdOnahXr565wfnQQw89xI033kj//v0ZMGAAr7zyCrm5udx8881mh+Z3OTk5JCUllX+/d+9eNmzYQKNGjWjTpo2JkYk7lMN1O4eVv/am/K3b+QvKYbtTDtftHFb+2ltdzV9QDrvYNofN3lbSjm688UYDOOVryZIlZofmc6+++qrRpk0bIzw83BgwYICxcuVKs0MyxZIlS077HrjxxhvNDk3coByu2zms/LU35W/dzl/DUA7bnXK4buew8tfe6nL+GoZy2DDsm8NBhmEY3i6miYiIiIiIiIiImE27OoqIiIiIiIiISEBS4UtERERERERERAKSCl8iIiIiIiIiIhKQVPgSEREREREREZGApMKXiIiIiIiIiIgEJBW+REREREREREQkIKnwJSIiIiIiIiIiAUmFLxERERERERERCUgqfImIiIiIiIiISEBS4UtERERERERERAKSCl8iIiIiIiIiIhKQVPgSEREREREREZGApMKXiIiIiIiIiIgEJBW+REREREREREQkIKnwJSIiIiIiIiIiAUmFLxERERERERERCUgqfImIiIiIiIiISEBS4UtERERERERERAJSqNkBuMPpdHL48GFiY2MJCgoyOxwRSzEMg+zsbFq0aEFwsPVq2cpfkaoph0XsS/krYm/KYRH78iR/bVH4Onz4MK1btzY7DBFLO3jwIK1atTI7jFMof0XcoxwWsS/lr4i9KYdF7Mud/LVF4Ss2NhYo/R+Ki4szOZq6zel0smPHDgC6dOliyTsjdU1WVhatW7cuzxOrUf5ah/LXmpTD4i7lsPUof8Vdyl9rUg6Lu4qKikhKSgKgU6dOhIeHmxyReJK/tih8uYZ1xsXFKeFN5nQ6qVevHlD6euiibR1WHf6s/LUO5a+1KYelOsph61L+SnWUv9amHJbqFBUVnZDDKnxZhzv5q7+4IiIiIiIiIiISkFT4EhERERERERGRgKTCl4hUacqUKZx99tnExsbSrFkzxo8fz6+//mp2WCIiIiIiIiLV8rjwtXTpUhITE2nRogVBQUHMmTOn2uf88MMPnHXWWURERNCpUyemTZtWg1BFxAw//vgj99xzDytXrmTBggUUFxdz4YUXkpuba3ZoIiIitqE+tIi9KYdF7Mvjwldubi69e/dm6tSpbh2/d+9eLrnkEoYPH86GDRt44IEHuO222/juu+88DlZE/O/bb7/lpptuonv37vTu3Ztp06Zx4MAB1q5de9rjCwsLycrKOuFLRESkrlMfWsTelMMi9uXxro5jxoxhzJgxbh//5ptv0r59e1566SUAunbtyvLly3n55ZcZPXq0p6c3VV5eHvv27TM7DFM5nU52795d/n1d35GmLm5lm5mZCUCjRo1O+/iUKVN46qmn/BmSeCA3N5eUlBTatWtXvjONiNjH4cOHCQ4Oplu3bmaHIh6qy31ogIMHD5KdnW12GKZRH/pEcXFxtGrVyuwwPFKXczglJYXPP//c7DBM5XA4SEtLA6BZs2aEhISYHJG5rrjiCpo3b252GG7zuPDlqZ9//pmRI0ee8LPRo0fzwAMPVPqcwsJCCgsLy7+3woiR4uJiunTpwsGDB80ORSxk8ODB/PTTT2aH4TdOp5MHHniAIUOG0KNHj9MeM3nyZB566KHy77OysmjdurW/QpQqZGVlcfHFF3Ps2DE6derE9u3bCQ31+WVARLzA4XBw++238/777wNw//338/LLL7u1hbfYU6D0oQE++eQTrrnmGrPDEIv57LPPuOKKK8wOw2cCJYcdDgctWrTAMAyzQxELue+++ygpKbFNAdDnn3hSUlKIj48/4Wfx8fFkZWWRn59PVFTUKc+x4oiRjIyM8qJXo0aN6nRH0+FwANjmTe4LDoeDjIwMNm3aZHYofnXPPfewZcsWli9fXukxERERRERE+DEqcdfs2bM5duwYAElJSSxatMh2dxxF6qpnn322vOgF8O9//5v27dtz//33mxiV+FKg9KEBNm7cCJT2EeryaGP1oUvl5ORQWFjIpk2bArrwFSg5nJ+fr6KXnFZmZmals4CsxpK3+q04YqRish89etTESMzldDrZtm0bAN26dauzw7T37t1Lhw4d6tRF4N577+XLL79k6dKlthuaLqU+/fTTE77/5JNPVPgSsYGjR4/y4osvAvD000+TnZ3Niy++yN///nduvfXWOl1IkBNZsQ8Nv/ej77nnnvJpX3WN+tC/u++++3jttdfqVD/aXVbMYVfBFmD9+vX06dPHvGBMVFRUxM6dOwE444wz6txyNy5btmyhZ8+ewInvDavz+V/c5s2bk5qaesLPUlNTiYuLO22VG0rvBsXFxZ3wZTb9YZaKXCP+6sL7wjAM7r33XmbPns3ixYtp37692SFJDRw5coRFixYBpR+coXQEWEFBgZlhiYgbXn31VXJycujduzfjxo3jmmuuoWPHjqSnp/POO++YHZ74SKD0oeH3/lJdnjEhv6sr/ehAyeGKxY26XLCVUhVHrKrwVcHgwYPLP2y5LFiwgMGDB/v61F7ldDoBJbuUcr0PXO+LQHbPPfcwffp0Pv74Y2JjY0lJSSElJYX8/HyzQxMPfP755zgcDrp37864ceNo1aoVWVlZfPPNN2aHJiJVMAyD6dOnA/DII48QHBxMaGgoDz74IED5YxJ4AqUPDepHy4nqSj86UHK4qKio/N9hYWEmRiJWUHGkW8X3htV5fPXJyclhw4YNbNiwASid8rVhwwYOHDgAlA7PvOGGG8qPv/POO9mzZw+PPvooO3bs4PXXX2fmzJnlHTa70J0qqaiu3KkCeOONN8jMzOT8888nISGh/GvGjBlmhyYe+OSTT4DSHYmCg4O56qqrTvi5iFjTunXr2L17N1FRUYwdO7b85xMnTiQkJIR169aRlJRkYoTirrrahwb1o+VEdu1H19Ucrjiqp66vTycn3sAI6BFfa9asoW/fvvTt2xeAhx56iL59+/LEE08AkJycXJ78AO3bt+err75iwYIF9O7dm5deeol3333XduvK6IItFdn1gl0ThmGc9uumm24yOzRx08GDB1m2bBlBQUFcdNFFAEyaNAmA+fPn1+nt5UWs7rPPPgMgMTHxhLW8mjRpUr5bmOsYsba62ocG9aPlRHbtR9fVHFbhSyqq+B6w06hNjxe3P//886v8IzVt2rTTPmf9+vWenspSNERbKqorQ7QlMLhG55177rnluwv17duXM844g507dzJ37lyuu+46M0MUkUp8//33AIwbN+6Ux8aNG8d3333H999/z+TJk/0dmniorvahQf1oOZFd+9F1NYeLi4vL/62pjlJnpjrWVbpTJRXZ9U6V1E2u6YxXXnll+c+CgoLKR31puqOINR09erR8Ss2IESNOefyCCy4A4KeffiIvL8+foYl4RP1oqUj9aHvRiC+pSIvbBzhdsKUiXbDFLnbu3Mm6desIDQ3liiuuOOGxq6++GigdUXL06FEzwhORKixZsgTDMOjevTvNmzc/5fHOnTvTqlUrioqKWLFihQkRirhH/WipSP1oe1HhSyqy61RHFb7cpCHaUpHrfaALtlidazTXqFGjaNKkyQmPdenShT59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kvSjH/1I11xzTUhfzyZDhw7VqFGjNHv2bN11111auXJlyHfRHj9+XHPnztXbb7+txYsX69SpU75/a9u2rYYPH67hw4fr2muvpdDlIrQ6IhDm0faxsb0NoWHjscAIFAR2fCEQVqrs47Q5jhkzhusURKmkpCQNGzZM0pnjAe4SHx+vwYMH609/+pN27dql9evX66mnnlK3bt1UWlqqefPm6fvf/74aN26s8ePHa968eZV2+9SU8vJy3XPPPdq6datatGihZ555psZfw3YvvviiGjVqpHXr1unBBx8MyXh46tQpLV68WHfeeadSU1P1/e9/X++//75OnTqlHj166He/+502bdqk7du36/nnn9eNN95I0ctl2PGFQJhH24fiNRw2Hgv2RWwAhS8EwoBtl5MnT2rOnDmSaHOMds7nP2PGDPLX5Twej7p27apf//rX+vLLL7Vx40Y98sgjatu2rY4fP64ZM2ZoxIgRatq0qX7yk5/ok08+qZHdA16vV7/85S+VmZmpWrVq6Z133lGDBg1q4DeKLE2bNtVbb70lj8ejv/71r3ryySdrJKe8Xq9Wr16tn//852revLmGDBmiN998U0VFRWrTpo1+/etfa9OmTcrOztavfvUrderUifmZi1H4QiDMo+1jY3sbQqNi4ausrMxgJMG7oMLX9OnT1bp1a8XHxys9PV2rV68+5/Nnzpypjh07Kj4+Xl27dtWCBQsuKFhTaHVEING2Rbu6ee82CxcuVGFhoVq2bKn+/fubDgcGDRs2THXr1tXu3bu1cuVK0+GgGtLS0vSb3/xG27dv1+eff66HHnpITZo0UX5+vl599VVdd911atOmjaZMmaINGzZc0GucPHlSd999t5577jlJ0t/+9jdaHM9h8ODB+uMf/yhJevzxx/XQQw+ptLT0gv5fGzdu1KOPPqr27dsrPT1dL7zwgnJzc9WwYUPdf//9+vTTT7Vjxw499dRT57yrqNtF6zyaOTQqsnUeHW35WxHdEnBU3FkdsYWvzMxMTZ48WY899pjWrl2r7t27a/DgwWfdQcfx2Wefafz48br77rv1xRdfKCMjQxkZGfrqq68uOvhwYccXAommlarq5r0bvfbaa5KkcePGMfmOcgkJCcrIyJB05riAXTwej/r06aPnnntO3333nZYsWaK77rpLdevW1Z49e/Tb3/5W3bp1U7du3fT000/rP//5z3knZl6vV4sXL1bPnj31+uuvy+Px6M9//rPvIvyo2oMPPqhnn31WkvTCCy8oPT1dy5cvP+/4eOrUKa1atUpPPvmkunbtqi5duuipp57S9u3blZCQoPHjx2v+/Pnav3+/pk+frv79+1s/D4vmebTtnx1qlo3z6GjM34pjJzu+4LBxx5fHW82/Nunp6erdu7f+9Kc/STpdpW/ZsqUefPBBPfzww2c9f+zYsTp27Jjmz5/ve6xv37668sor9corrwT1moWFhapXr57mzp0b8gvZBlJQUKBRo0ZJkkpLS6P6pLm8vFybN2+WJHXs2DGq34sdO3aoffv2io+Pr3R8h9uxY8c0cuRIFRQUKCkpKSSvUd28r8h0/kqnc3j06NHyer3avHmz2rVrZyQO08jfM1asWKEBAwYoISFBs2fPNnpNoHDk8MVwctit8VV04sQJzZ8/X2+//bYWLFhQaedRgwYNdO2116pbt2664oorlJycrJiYGOXm5mrt2rVasGCBvvnmG0lSSkqK3nzzTQ0ePNjUrxJQeXm5Nm3aJOn07je35XBWVpYmTpyoI0eOSJI6dOigoUOHqnv37mrcuLHKysqUn5+vrVu3av369frkk09UUFDg+/natWtr6NChGjt2rIYPH666desa+k2CV938CPc82g1j8N/+9je98847+tnPfubbSRmNGIMre+CBB/TKK6/ojjvu0IQJE4zFUZ0x2OR58F133WVkrnLq1CnfIuHHH3+s9PT0sMfgFiUlJdq1a5ckqXXr1lF9Pcns7GxfB83EiRONXey+pKREb7zxRlD5W639iiUlJcrOztaUKVN8j8XExGjQoEFVtousXLlSkydPrvTY4MGDlZWVVeXrFBcXq7i42Pd9YWGhJGnkyJHVCbfGeTweff3116xY/ZczeEerffv2STrdFjNo0CDD0YROdfPerfkrSf3791dpaanvxDGaRXv+Jicnq3379tq6datuvvlm0+GghiQkJGjMmDEaM2aMDh8+rFmzZun999/XsmXLlJ+fr7lz52ru3LlV/nxiYqLuvfdePfroo1zT6wJkZGSof//+euyxx/T6669ry5Yt2rJlyzl/Jjk5WTfccINuueUWZWRkqH79+uEJ1oBwzKPdPAYfOXKE8fe/on0Mls4cm2+++abefPNNw9Gcn+nz4DfeeOPCg68h+/fv19at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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from typing import List, Callable\n", + "\n", + "from bitorch.quantizations import Sign, SwishSign, SteHeaviside, ApproxSign, ProgressiveSign, InputDoReFa, WeightDoReFa\n", + "from bitorch.layers import QActivation\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.inspection import plot_partial_dependence\n", + "import torch\n", + "\n", + "\n", + "def calculate_numeric_gradient(x_values: np.array, y_values: np.array, x_step_size: float):\n", + " # :param x_values: a list of equidistant x values\n", + " # :param y_values: the list of corresponding y values for an unknown function\n", + " # :param x_step_size: the fixed distance between two consecutive x values\n", + " # :return: a list of numeric gradients for each step in the function (with one less element than the given x_values)\n", + "\n", + " # initialize a numpy array with the correct size\n", + " numeric_gradient = np.zeros(len(x_values) - 1)\n", + "\n", + " # TODO: calculate the numeric gradient between each consecutive pair of x values based on the given y values\n", + " ### BEGIN SOLUTION\n", + " for i in range(1, len(x_values)):\n", + " numeric_gradient[i-1] = (y_values[i] - y_values[i-1]) / x_step_size\n", + " ### END SOLUTION\n", + " return numeric_gradient\n", + "\n", + "\n", + "def reconstruct_gradient_function(x_grad_values: np.array, x_step_size: float, y_offset=0.0):\n", + " # :param x_grad_values: a list of calculated gradients (based on step size `x_step_size`)\n", + " # :param x_step_size: the distance between two consecutive x values that was used to calculate `x_grad_values`\n", + " # :return: a list of y values for a (representative) function that would create the given gradient values\n", + " # (the y_values should be zero at the middle element)\n", + "\n", + " # intialize an array with the correct size\n", + " y_values = np.zeros(len(x_grad_values))\n", + "\n", + " # TODO: construct a list of y values for a (representative) function that would create the given gradient values\n", + " # HINT: start with any arbitrary value, create y_values step by step, finally subtract something from all values so the middle element becomes zero\n", + " ### BEGIN SOLUTION\n", + " y_values[0] = 0\n", + " for i in range(1, len(x_grad_values)):\n", + " y_values[i] = y_values[i-1] + x_grad_values[i] * x_step_size\n", + " y_values -= y_values[len(x_grad_values) // 2]\n", + " y_values += y_offset\n", + " ### END SOLUTION\n", + " return y_values\n", + "\n", + "\n", + "def plot_functions(functions_to_plot: List[Callable[[torch.tensor], torch.tensor]], xlim = (-1.5, 1.5), ylim = (-1.5, 1.5), steps = 200, y_offset=0.0):\n", + " x_values = np.linspace(*xlim, steps)\n", + " x_step_size = (xlim[1] - xlim[0]) / steps\n", + " \n", + " fig, axes = plt.subplots(2, len(functions_to_plot) + 1, figsize=(15, len(functions_to_plot) * 1.5), subplot_kw={\"xlim\": xlim})\n", + " \n", + " if len(functions_to_plot) == 1:\n", + " axes = [axes]\n", + " # create the actual plot\n", + " for fn, axis_idx in zip(functions_to_plot, range(len(functions_to_plot))):\n", + " x = torch.tensor(x_values, requires_grad=True)\n", + " y = fn(x)\n", + " y.sum().backward()\n", + "\n", + " y_values = y.detach().numpy()\n", + " \n", + " numeric_gradient = calculate_numeric_gradient(x_values, y_values, x_step_size)\n", + " numeric_grad_function = reconstruct_gradient_function(x.grad, x_step_size, y_offset=y_offset)\n", + " axes[0][axis_idx].locator_params(tight=True, nbins=3)\n", + " axes[1][axis_idx].locator_params(tight=True, nbins=3)\n", + " axes[0][axis_idx].set_ylim((-1.3, 1.3))\n", + " \n", + " axes[0][axis_idx].plot(x_values, y_values, color=\"black\")\n", + " axes[1][axis_idx].plot(x_values, x.grad, color=\"black\")\n", + " axes[0][axis_idx].plot(x_values, numeric_grad_function, color=\"darkred\")\n", + " axes[1][axis_idx].set_title(str(chr(ord(\"a\") + axis_idx)) + \")\", y=-0.3)\n", + "\n", + " for axis in axes[:, axis_idx]:\n", + " axis.axvline(x=0, c=\"lightgrey\", zorder=0)\n", + " axis.axhline(y=0, c=\"lightgrey\", zorder=0)\n", + "\n", + " # plt.show()\n", + " return len(functions_to_plot), axes\n", + "\n", + "\n", + "def plot_progressive(axes, idx, functions_to_plot: List[Callable[[torch.tensor], torch.tensor]], xlim = (-1.5, 1.5), ylim = (-1.5, 1.5), steps = 200, y_offset=0.0):\n", + " x_values = np.linspace(*xlim, steps)\n", + " x_step_size = (xlim[1] - xlim[0]) / steps\n", + " first = True\n", + " for fi, fn in enumerate(functions_to_plot):\n", + " fn.training=True\n", + " x = torch.tensor(x_values, requires_grad=True)\n", + " y = fn(x)\n", + " y.sum().backward()\n", + " c = \"grey\"\n", + " if fi == len(functions_to_plot) - 1:\n", + " first = False\n", + " c = \"black\"\n", + "\n", + " y_values = y.detach().numpy()\n", + " \n", + " numeric_gradient = calculate_numeric_gradient(x_values, y_values, x_step_size)\n", + " numeric_grad_function = reconstruct_gradient_function(x.grad, x_step_size, y_offset=y_offset)\n", + " axes[0][idx].locator_params(tight=True, nbins=3)\n", + " axes[1][idx].locator_params(tight=True, nbins=3)\n", + " axes[0][idx].set_ylim((-1.3, 1.3))\n", + "\n", + " axes[0][idx].plot(x_values, y_values, color=c)\n", + " axes[1][idx].plot(x_values, x.grad, color=\"black\")\n", + " axes[0][idx].plot(x_values, numeric_grad_function, color=\"darkred\")\n", + " axes[1][idx].set_title(str(chr(ord(\"a\") + idx)) + \")\", y=-0.3)\n", + "\n", + " for axis in axes[:, idx]:\n", + " axis.axvline(x=0, c=\"lightgrey\", zorder=0)\n", + " axis.axhline(y=0, c=\"lightgrey\", zorder=0)\n", + "\n", + "\n", + "\n", + "def quantize(x: torch.tensor):\n", + " backward = x.clip(-1, 1)\n", + " forward = (x.clip(-1, 1) * 3).round() / 3\n", + " return (forward - backward).detach() + backward\n", + "\n", + "\n", + "# plot_functions([quantize])\n", + "idx, axes = plot_functions([\n", + " QActivation(Sign(), 1.0),\n", + " ApproxSign(),\n", + " SwishSign(),\n", + " QActivation(WeightDoReFa(2)),\n", + "])\n", + "plot_progressive(axes, idx, [\n", + " QActivation(ProgressiveSign(use_global_scaling=False, initial_scale=i), 1.0) for i in [0.03, 0.1, 1.0]\n", + "])\n", + "plt.savefig(\"quantization_functions.png\", dpi=1200)\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 ('venv')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "449ab358cbc9abff7c95eafc39955d97c1cf480c5202edcd424b61e46b87f27c" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/quantization_functions.png b/examples/notebooks/quantization_functions.png new file mode 100644 index 0000000000000000000000000000000000000000..66dd3ceb8fdbb3f5e3c2ac4c436d4acf106393ed GIT binary patch literal 1264988 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�_BkN!1ZDkGrs^T_ri02T-@fLzh)$f&oobl zHH)YJ2^_@uo|`)e&;1Y9p2kW?*sDqMXS5sUI%|)CVMNxkgui0-9~LM3wN;Pt4%=KP zSfNFSTxGQ;gNCI6H(Je!onV&a+dVrAT;QCk8#^o;pDC_)i4zaF^sEB-b2=wKu+A-w z;0RH+Ij;gG!_`t##Nxcqxa~d=`mnor@F59{-9aKm(Cx0_8Vp9sgmoO%qr-d03r)fW z40R>A6)#_HI#RN-TtP@Ma8zA4CrqeBi_QX^Q)2?V^p*cEU!7@zJi@DGZx1Bq+E{*w z5s$ov5oay>a2pCCM9PA22t8}p7yq2G$dVu48l1n#BdaXOx~H`Hgko^3giGyD^W70+ z<+B;wljkx>aVrsQJWXN=AF3W4Z7jzuyM-tvmDO5e~85Qs>JJrL!xfqUFE# zo^h($O=_I4h@ZE@3dGV|W#eKnQs{?&lKuFw8wXOwvJ0tB4CO-m31RDvbxdMyjEsoC zEU4rg`^?%QYfrFBStyk|VWn|1eK$2yw3V$|Uq2D+t$)g5_yFn$%qGkFc(o|{hE4k_ zgR#>_c41Lb{Hmj`kVf^r6S2WutnRPA$IbG{Dkfe&5&IplO5$b3_HNg@2-I>XyR46A z!CKFJvqRael2z;PRn3W!PTej=1*o;Imm`^xt|J*&bqmtz+O>2i1_%8FarckG%4epU zm6ffy!j$F~7O}Zxw~A<`#LLCoZ7gSZi`7>G%h1Rwl+!!gZgrEx^?0nN5ENCh-@zGk zN!rV@c`(8X?KMGns^O<~o>9Z$qLEo5!0bI@kSKIHBdu;#tm{EJPn@pA_je&LxGYdc zLP2)rTDj=e%buKGb|5}_B35hfMCr`sap|!^Wt(UgGMaL2D4XvU>-OWz=?UnkFu zXmyBnnO@ArVy3c6gUQwR6qCma1!Fb$-rv33V+tQFWSJc6M4s!k8on=PiEB=zYrjkr zFEcnKsWvU=S`plKus;CnLHoz73wu1pKi=5UY|&Twgrkui1l-O*_{x`RjxG)Z3kFRP zp^lm!^@#M|tf>F_)9JeVqP6jY#mAwk1O0|P!KnGtXaTVw$v}ydI|=nCI6uK^cY;$@k_!#Mq<$QI}RT2Xw25hVh7RfwhreOfT}CM*YxJDl@H9we{8cXveL3J z#RiT>phm;83y0LnD+JcoTq|F%%59O?C(5qc+0l0u8`K!>hunhQzFr5Hr-A}R(_2d1 z0zu4|L&8mji7?qQP@9c$A|w%(DR4%QEfeLw6JA9Aax zyDnH3x@r~kP&+NlSR(BhUYhLub~kJAL!%P>2NI_4AF-a}bVnXlV@5fBKO4Y8n5$yO zest?O(bkw`R?RNzVA$dK;lM!v>t-L1S zVmzgD`mU=}K`rG-0w6Tiwo`CGohLT*A)CXaDG=VDw`@2`w=(inYfe%A#Fk{j9iF(F zcPSbgCMIilQ0nvJ-N^dW)!wdGsCr2q`qSH8KZzQ6))EYUpvwTHnn- zb~9mRWy}0`ohV>lF|(I+ttV8FVt1YGzo$|%7%p|4m>3DHX=1MSOK7<%h`dnk@$q9o zE)GPM)<9gbrJyq$10sY8kx0sWuMr4gw{gnrxIfp)w3RCQX6~3Ww0KShtQ7 zWOZ5TXm;m|sr>q(Y9QL0aEI=V_^oMECi2H^F)`w(sYmF&>QWDjkc4#_8?B)0K#jo< zkNMmpa^mXue|?xD)}w2O0;?Jg4iQG-z+Fn;wy*$9ki^7GGw9P3D%$5d)S+$#r6A94718ZE$p<{6vaUk^SAZ=-#+joN^}lR;Dga2u=Dl||I- zBXqi6t`hY^KcCnZ)QOMC3;jiFGc5sQZa9mGQMYcFlmhEZpx!fWLp{=Uf=gA8tGh1!tGyyq%1T$#W_o^aIu>%bnYPEOh?xkIKQ* zM?w_tT>w6|a)qP!Q8vzNavKWMCq(}S;%`w_qAHUMO<(<~5+zI&JSgNrOpIU*{i3y< zb&b)@?G|83dp~Xh8o0_vGNJX$Bg>U{wd|XYL?3j%C@MXaP#-IN0$LV+$L6R4od3iY z?Ux%Xe|wBdl0dl7Vg$s5I`i8FpqNl)bsyaZ;8mim^7MsB&UYb&EI}8F7UhSgtVpWH zk`diPbrwBFp9D-#$GYvuIkaS(L8C6@2;?xoYa)-~fWic8coO;Z}!(xBg zq9rkX-*_3T&~%@k)!;)UwgIoe5$4;wa31oO*K=`Jx6!^DRfTk$DAO1}_3_o^32cC1 z`Ox*flrjl8C)&0vDx&?M`ypnZwPpDN-d3AErHlCV?lhf;J_#|<8tM%~eEee}%K>E_ zqp-lVnl+|bPTxmX3PG6sPP-5`*Nzh!j-{v97l9zmy$oR1XK>6(w^r^!$wD*L18fKG zjxtNFtvHiEvIN*Msh+EI9c#84YTV^JUc@fpYF9jwuLI%@9^?=H$m&Z97UqQx9t)+U z>D5sjUFXxZh{35%<=weHFyjLm7c97O5GlotOZJ|~*)1#$BB|1I9lX%g-V7v78gQb# zKhL#*>x};Fy5Iw!24T|uIn~U-GA6wgs|9~bds3Q^#+Ipk)MDRFkLkO-ckQxzG7RR* z<~lS}TaPTuwG?3X8<=As9u;zadvA26iqkTGwjG+&fY4W@gr&;c(L9Yzwo`|o+^fWZ z{bA7f_N{+@6)6mQRNA45oZb(z&)xn7mswy3qPoQ=z=+G2!z|1>)@CFo>Akir?z=QI zN=h@aDDx#N@wC==DTbVJsCjU&>sr-4*^%_p<>55C?bavApf6l`ce+uWFc7=8f<4k{ z!Y(5fThLulBibm{n(q+HUon^OZU6B#`}H+4ZlKOY4TLH6M6!n(#7OVuSq3B6>_ZKh zWzGtLyiY~~SBDt%@Ln7#U9CA9=}t_PFA$R*em)~T_4-Js97!idGv`BQ2_8iL8nOk; zAK9x(i|}`Xxu_O27hS{xv7>9R^wYwwGa&*MQk>-^HM+{AeTBhFP3onAfZaWgGhA2g z(idH;jmvTfH4jrXPm1(z*Osxh&rx(O8O@2A?6AdwSzXIx+jijVk=jUDLJ!Dp35%7q z>bhQSfq8w0Zrka8Pu1rd&ZTK*G=rZZm>KUbasH8_gq1v73_9LY%<*oOXnKcBFD*RKz@F>Qb5 zHqmpnHB}2EJ>MlosuxP6&I(^WOxuR{wM3F$@AX*Wkv)-924F)G=(~1IX}7@~-}9ia zJM^(!Tf|15^>#OYNlaQ_&&fE3ms)j)KI5|e_v~EiPeb`RUG1g!JO#V!P}&`&mz!Rw^(@%@A97G7Jg@V!!-;3j*=Na0`zEs}0tKp=b|S6---n zjlWWr^G2sapJZg(+DatFW1+>(2-MoV)M86Kp7`)+?n0~^h}XRa6F@Y`=n}fVFzMXw zGY_f1op;}jt4%o;MDS}9ep^wde z+Wbx5c+LE|^UNB~%C>yDXq8NQqb!)QC?FRM<*DIjp9JJo0n<@rn~8o_Cr}41K4DTL zp{W-10fBI(iNoibmtXehoSz?Gj|yq)(|1p=Q%!WV@7d%>#Yubl&OA_DTP3$-nN;N2 zwr5f&c~m-Gr#*7pOh7qvW-P|Kr0zD`%ymoaV&C!3o2?IJ(^~Gc6zlD@6zBejaXAU2 z0_+|zVujAy4SX@ul0nATemh8R3TpPrDuV`6yF8G#v;p6;j;;d9?4*LPo(1x=avMaK zhYp8;j$j^bUx2oO*21U~flP~DQhpE!frygaXpccCW5(=LDojJ+Jh?RsO7jxw{eHpg zWUGy+OrM{fcjYz@v*`io7Bn1R9$t4gwAt?8kCbt>O9}$<^#5pwI-u!M!W$QW)Sfb@ zqUr>?m#ET^;=8UU$A%-Z(qiQ0d3Aqish+B+w;k|nnBAvRj`WKDOj&@^!z}|Pe<3+PxM4`ug#50dA|DIV@5$%X})YuDIR2Hz6A*$ zP*@Mr{bN;h@oVQ%j8P?z+vKYq5o%P`UC`X3yCz-f$;ILyqLvGzCVU+#{dNJL3RKto z=d_FpUss8VQt#E#T++&JG4qd!5tggD*l#f7zI=u3w+#%!ZdykI(xua8A~(2}ociI^!RJ@woN-U zU#wSZJtR{f6B#R5dTLD4O(0ei8PMY`1kIO3%Df0m9IiDj6)P6&*%kC$(I`I#vkn$s z0yq84_i89C{rji9^YTj5t~hr@P2x5;Ds~v%oq?gIl5G@@P6(bOWGlQsnExp=46= z=^HzyXVD?Kvs2Z{Ah!^Bf6!!D3OUUr;M0eYcT}T;K<=irQ+smMwftZ+fage0HQK z<_tHpHz~R(RXJj1yv_uZ2OPniM6eb`6-N5%DnM|NiBTQDzOwkwPRKSzIcTT0Lj4ZP zQ)@BuOM23vZ)?#B774Rkk_P)a9o6Th=lNnae}{4xm&g;y&R07KwU}6BFRPV#Pm6j4 zy_tmR_l&OE49-G9>^sT=DU0I;Am5>l^Hc)91fi-563L-NTXDOtm%Ab*M9Ae+_w$Au zA3JOR6aW(GUMmYWi1;iGbgYtvh{>`IPYQcZ(n}1fv`S~N=Y_W~IHDx$_6uebEvp;E zuz`d)gBDdke1aJ^4o9A@6S8YdA6;#UPwK}e(bOeg$7Y>N4A^m*v>o ztSH|AvidhVURNnr#>2P%5g)nlCf&h>&!17VsjweBb#ic}XWpi~Wd8XjDCHe8SxJLuzlKE1;C?H<-(*7@FeAg237#0m$a z_8>@V8pI5mNHe2)q}ItW=bo#{?AQA5vn~y%;igJS%}~@|PoCdn zqD)HC)7h8wM$pnc=pZX<4i`B;noD=JbYYFrnS3v4F7Ilj^WvaLx8YNTpi%D+x1dUe z<$<-o(S?dto!e^hae`~(Zp3mzTw=&wFUPm{q~`iL2~j;|NY!|mMUp&=#jAU)bgOiM z(e+ytmJCu*Zzn3-78kV(q9V1&I@_K1nrdl+_4uS69HCWj@7DplX;BHfmnes1IOe1S zu#tpUxtQ7-hi<3-%o59KEgcIGx1}3_(Y0Vy$$Ozxe^T>UVY@SOYv}}k}m9Vey0gqQ)!7offiShSzQRJ2JN+zM%_6kfhv&?aNM)QRd zqe&-gCSbG}Uv5Wb5G|tvIdVz2SlXQT`c=HkvG24Kx4sCiZ?nz}gy1}f8uJY8^PBbF zi0~OjMS%2EH+Bzm-_;`QAXv;C94r~Hg~mO4k42=8)bBw^ z1bzxoslEvVU;suvTD72&cs<+0AzmgiB`|rR146ZpKC%5fYN95(g>sM_v z?ioGH=ZNUQI7g~k;fH5)`7@sGvrW;jPJFKQuh)Kuem(Tv;eTJa@ZDiAC$%{rS*$pg zynM6f!p@&B#4XFzjkp-~j(A{v;wF80bS7hN#uJcq)9fc(_Kd%O$ho!{9}y+~K_m-} z<6OF!5{Y1seQfr2V00w2Zka`(K1*ih8fFQ*CZ2Khyrxl_`q?})h5g;`X5}ueT8sXH zfnDSe%vM$8`Q+#AX_-_(1X@H+nF{L|(K_&z%(o+Gt$MBIF^8m5L+MA2cqOE*EvQtzj=gdOeu~ZExCnAdqLnYMyMDN}m`rRz_M*sHxRv*2C+kmB7ZH9;~l$K z{r0{`OYfQwunW1v`aZSet!YAQA@TJ(7)il3VINQY>IY8C{-Z(9j^9m9qr+roXNDHb zrt0=B7MGNe`Z?Fm+P0;mZ|`PnZfy}8>C)-U#|*FV}RQ1dP`Qw5iIBBQKKOKeyO zJZu@Ou)35*sxet}*|Uc&yn!d~kt+Cfo4w^yRn=-|LKyquKL3a*0P1LRi-w@rhe8g+}Sio&74jJ#W zJD3luWN0fpIyyqy__JK%-6!tjC}xxs_?6T_3Gan#t@}P<^pi=*AKo0Dmr6@Z_026)_M1ZmhA`g`0=zV)$W2OYWjm=oAp=MBe6<2-RrMs7o1Xv(O2C zqqv`2FZ$W@JXXqyne;CGQq&K!T8gkf0)@T4Uc^lpzjHtRp-#$I? z%eFEg9l;8hW1*#UUJWBjS?3 zx)MBeR>^>Ihhd1k2;Jj5eQoC?twrR1~;yru-i@A1$vqGD%!k(JeSRL@Lt!YVf&zb6?ma z5>Qa}eOC!68A;dC=ajHYqXN{mDwazsq7#)PHaCm$Mcej*Zy)sTDB4rQgjRsQ;bu|;X=p#=tVhWV`>6@{YRMe4>cG^rFU)=3 z^T;lu`HYJ5hN(xX$tU`00vm>O3USw zH{!SLl_3Z#A`qJWl{c6cU-QU3?CI&*xY~>tem#dJK?1c>jf&xrbk#Is+FsVq!>aX% zF+X}6z0EI z2h>-&cCrYhuXC-ubo!IIZhg57{L&0ZpR~>QC6rH5Fh85_Yv57%fQp5HR9mRpl?kL) z@LCMe+qBkyxEc%=|E^z8pO6!A0PnM~>n_gUI33fUpUm&`w_tr{da{aH2fCTViUNY- zq;f7vWzbA0@lf;CheD@;s$rDRB-LK_={li+L0!nlgR2d67u&dP*d%fKzP@r(8= z1|@D27}?c+S#2Z8h3b1AxgN^)v+*V>o1_*~)DnRxeb$z`6RP!>f4fAXZ;uuB8h>t& z^&@^d>BsMOIk$k3{Xl4>NQm zGMHUJe6Qb)$8iJ1qgL+0$l#dhflyForlZrN@hwG)o+0TfIWsjly(@yBPXqqcP7Mm8 ziiW?$M&&r?yC-}LDFRUDnkJR*uYdK_`*hvxCz}dYn-A88Y3WeT42Z_Y#;y+@BBqTc zT#8edw@^;gQNb~kl) zMqAW{Ypv_Rx_qc#JJV2a!)aL;ojv)V-p{XgZCNHJ)P?-a{-Dl98Jeh;y0+rhT1J0~jvZ3upMz9*UNOxGa|yyO5xmx)64i94WMh0;YV}hGIsp#Xv3wXpBsX z6>F@hkraZWE5ro`yG#+A?f_5 znf+cRU&tA*S#$s+j<_H?EjUk8C$0C_xB(XhwcvF1oY5L$eJ-k;f^9~SS0^)_(nlJr zTW#{!y(MbPMyqw#)$of>fHPMANBtrLZrkiupT7MwsW_y|7&Pg-V5#c^_mrGmgGs?E z880+Y$mWM6JeOC9R4iyxESLC2YOM?GxxsWyb_1hdd`3x$r2Hjd{V0`31S)+sWJE)@ zpW+`NzG_X#oBM8;O$0Zq@gF^8Mc0T&I}oz=j&WptXH@XskQn5E%JUpeS)r5YMFll! z^ldJ6PVtc-L&{|nCWu60$|htZqO}_p1+{BLeLWyU(*`=QhIgqu>D}@053UCdiSP~Q zR~(meQd4vS-UBzQ9_Rr{;%Kx2rd4qdm&Y;mG?zLL&dBJC4M8AQ@9Sr1Q*J#WC|8iG z4gFTX_TcWb#ZbW{8~jvp|+R($i$mOH1G5cO5bv7tU=NB-vn_l$% zgehX+B7`a3M)9Dm(|NAQumKlEHRWbQy|mRde)PSgJc@HXdJhL8k6I~EW7-}N7Z*o$ z86U&{XbS{;{?{WH)E%hwuI~}8x?U`wY0feR>-{pQvyw+ z#BEppd5F9e>=~TN#Tyi(rMO2&2iVet-ZNi3g1n8M;1|(AZ8Se=l3)^*|F1dn8q7FB zCbRCR@&8v%$N~O50oeY_=USCNIi8yN>D#9g9({5zrg1F02M>e|eEgX-#}lao3|#0{ zo#5rbm>_rwWWYp%s4M zkbZEC_kdyKGahs?7jH=V+3C{c^;&)YKKWdRb{fSLG*<7EzeKStpwL;HugB4@KB{Nn zh9;;muAepVPLcI-><|}A#G^t%bdYjAlVABC%E2W;F?SR#k41)=+k_=KzdYH>Lm_79wWxg%fdnv6IC{5{i86K{y-YUV_C+5jl5W5VQgQav`K0j`} z#0A+Ozw_2gXGG91r6gIT)0;&RyMEc|7JdV`Sf9_0^65|L^W%4KTXi|r@w3W!yH|m< zS3jH90{glG@CJ*_B4kT&Va9rN)M=$R=pU5tfUUDfu7{ooAOJ6zg#$ zw>fV3OMbtN-sD^{?IFd@gE97GYnh8}+*0L^%kOLw2l4~iBG%>x1qtf6ck^;HBbK78 z*u*U7nV8Mz$@2v8PDCvuE4izRPQwpFxUM>2CR@~$m+K>0#cj1atS~AgLrq$(ObPK{ z9Cm~pN{jb{G&ko$<&J6IifP{6*c-1*C;qvG11ropQscQ4PC#h=CKN#w;?j zq7&Tv?pv$x__|V$4KC50gcYWtYL}I4ODqZGpn5~?)oe6j+^N^K}YxX z_z6>cFt_rC@sQ)xerq_BWFesIM$^8`O2oH$=0 ztC(ZeT`Wb4ofwD&33LSTDERRYUIV`+8e?DW<8`|B=B{1IBAyGq0G58SMxIwC1d4#M zAttgSJFZ@}u}OV-96`RZVPt?6Nz?yff>*r%o=|Df&cLQPml3B41({$i1tM?7i5bg?W~yG87X3`4I+bWlYD*=W*{V`SK?|w6q^5d;%~627Aab}5v3Ka=6tj@pys2jZH#y1FKJ0n7*d=SR^E zU{Z;@wno;{*{IR>)0+odD}e(CCMw%9Jf;Io`1rGYrUy#L&vBlFh?O*fcN~#qrXR-4 zKMWcGW%N8oGCc4ly$u~D<4O}Bs8#}6Xj#}L~-{QUjk5-hb&EmZ??8&7-*!m9*ApcY68{v)A{%qylenO zW0mXJB%G~fz>;rrSy@@0N&E~) zk0gn-o42eiQWWVznX#k$?Tz3sbf$!dqXD;v!F0Bm%O9OXvDuN@j0FX90o0&XW*<+z5A zB!Y)eoDIFNCxOqgnCs`GR?;_A8G)GDo3Eeofi)u|143jC0`>zXjy|@YdpJ_5+Olcube4@o}am0N;binjw?}cF0Z)spc{DJixcxT+gRR4Mh>vrq)fr1BDJ-}i!26Q{+ zI;Pre)JU86NG&fVHduM+9WA#&OzLS$O3DW?V#|5#Um5GgqXm6ZD7_I2rfeTrHwNN$ z-8&EF9|yp)S#ICb2vAm+X2rTQJnaP^xWp=Htj2)-4aOK zt$+==ZIu@p{Z~XHcr|_bazUZR+S~gSFc*SmTTB|1DQ zDd}!BWfFH&HACCh{k(yJL6wkc!tuLD+*Sx{F%3KSYkmpy_leKR3;jAfGhn@NY;1H`VNcXP@F^Q<(=u{w1UB?k zED4{8>?tK=?v6ioI0U?sa{!ksWHC3YQr`FHy+>!W{|W!p9^n5<)5eyTmdSes1qHXQ zD&?m7)|N~>me_iqe+fTTlE6gqlaa{ ze_80p+v2gZiH1OsL;K|fh?}}!QQ9je1{emm-LUU;0_OLb*Tk0~HUhB%LN~%vPm79* zp#D~woi;x+0LC)$JfzhgPLGvBPR^cu-h8GV&0a$nJp*HUs{B#-YI#S;)(R2Cc0bjP z70U#5UNft_BJ1r>RY6|jtnAXIs~#+CxcEeing_sc`$e>!F~6Wzwlo~$3DN_nTjDVV z+{!si%e~lCkQYZN^{~7J9H-^E^f)0Npo`!YHqS%UF$3Vm5YI>$ukq^(fBmqpV53dJ zGGdf|M%(1Qwzf71 zN@PotY8^<}S8n*%0ozx}GGWOTQ}?NjHSRx*^^s2y&jUqp@F7U%dd&`RtOS7I^`Tr4 zc{uf=)yp#nMZ%Ac4A-t^W{!@H)<)ngV^@0t*#@>0xQjX!K>DwPwv_zGt!!XNe8czOb1UOLY#IKI%E`oOek>c zE*>>16u^2DB(X+pKEo-I&OIKH-r(cBx(IEv%J_QU;@{{;U^bhsohBmn6byGCSV6SX zbEc_&an>a3ixO4t%n^)w^&BjfeqbFN8)F42jbJ(x*d~OP3dy%^50CPgZPiHzCK`-% z*ChxIbmjY9UV$_Oo+g4@UXYj+mhZ`p&s)1FSDMa{JIacfZ5anIisS2x))ufgGcppk9mO! zmr(EO9I_oUX^P%i)zRCGV#N3>7Cd!6D^NFEOFt@tF zZ;)gA=I*F|(HTjd^qTRbK_%MhQ2Iw|=6`*@jf>*QrfVzGb42yzp)r1L+xn}?gCY}N zCMWa|a%RFJV(TUzq=pE9)uftL)#w}>OiVghp-ua!s_Ufb`iKb{mEy1)hx#<_67BBp z#ueBbR3RlKB!Js-Ee|{deg|5r$}0sw=wf*N%|75N-){wL3T(-$ykov!RvIQRf30`x z)>K$TL}VS@JuQ$Wsld7BPN8kQK6gC@#is-_j^7>CD-rG7FNi7NiLY;a6OijeY=$`I z`z)tsZiL<%lfeo>bvxuS3~{by=aoWZsBmEcX;Dqr1eo|Ljh)#@lQT1=!4aG%?_y+ z&@HDsn7tE1`M~tDfuEoFa_EV(q4OnyMb5ROP><*U`61wETk500dq~SOGgEn|zbgi_ z(A&%7b>QPQuCIYaz6Gxu-)q@W`#Mqx`A>ahIDt2khUoxLqnuSuTL>uDc zGr-#c{vW=+11!obSR3P2udxtgK}0}fK@>qmDbkEaEQkbk8tQ}*!dqV)v_GhNy|jd!!}vaCVIi&DwQjz_ZtMezL+$ArcU!uQZ?|+=YjJm3 zs@UY@#Kf1oo*Q4Bv|1fQFE&C;MsQ9~OHc5@8t^Gv)s?%sw1)qF$Hiql{B3W@r4L`z z$q&D9RtANE_26G;_Kq6*Vn+C_`@{)UN%h+i$_YFIMG#?o#s4}jFAVkGshxv+7q+e8 zu(jK11^#?G9037*f046a!0s!h>QKohzto&S2^VSb?|`bcnowyhn%uqMVg;YDp+K=a9ttQlhTtjxBhk%DMLp0599*A?&0lNi?p9Lh8HFYZwo8)@bB?8B zcB3D<=4}gPDT#2a-+y2v?pJ1J-6fya@N+KfGdKs13Cuz`WeeuAJqh1N)8{kBXLe38 z#OgPOk=okGmH8B7{QBgh{M>N`(kFQCm8H!q1m5JYf|bCAxtjH6mFjX)GJPTvVrLSt-bLZ{>=net#Yqw8MhUo{x$cbotAe}sQ$-M;(3v&qD||NDm6g)Z z$H>UIua?G&74$6Se?c3J>fE`?3)OT*G)6{c9$jW*5fr6>BVF^4z7=g`QoLC>n>2$Z z2)W{CnJH1~O!7MfOu`%U3{|on)(2xl_<7K>>e+mM)1;gBS(jwxUK9Pazw47ra>AAd z+`>x`oR8Q~sz+#UquFSD`Rbr)scnxp7cjXH?kio8(U|JC)IzUrLEQJ5csTmsH9zAP zG$2NA&o|RCX%wqfGGyfZZ4g+=`_Q%94T@IXgy+8z{~pMB3U>3uHnRVtS$?Ikd(8@h zFNWZ48^1%8zB1j}8=N=MDocsJG^0^8g!Ww+?7O!?9v&W&CXmv!oz9ASn=miTiNdcS zz7#NdoG308Z;S$n0P-o)#>hPvh%zFSYo|X1Bg6#RZB@iA*$U6Sp-%xl_gaj@d z62NGU1@Ul9jMP-GGH;vbh$JDOYGP(@v9BV4>ndpYT;xe262=CJlkc1 zQa3mF&{`^%O5=HX?dzlA@|M^oWF_U9_Mxo;Yxi?^|XjB1Voov)UAJ3DPsxi4R zhWHRRnwFbCJV3$}dwavc@PVbwkf4|QyO z{QQ|HfOBt;JJNyAcy~tB_iX?uon==7Xtx*2LJ6d^h4v>$%ko?_b1ydeSpAZ6^$?PPRNr+aK07YCW6 za5Md%bxqvh9pUT?@cVIy8)OYxhcNJ&KRp{{{iHMmy^KZB-_Z+^vMAm^M)FTIg@w{o zZen5>Gv_V8N^Ru(4wIrVS%|`}3Jm@Y6`OS*ZX%9I4UCJDWw-RxVN^c~O^d)$9LMw$ z3U?)&*N0JdP@(A4AM((Wh|RBWYHC6Zb)OFc9|rlu0mpmta>$e>O@cqsNyOL~8gdKH z-I1w%=iO}C^#<4oVC&b^Nek{#fM_^X_JC*8^&o$1NIqK=;N#u95Q*IEmk$uX5{XqH zS0AlwVSIycIyWr#J>T4V(BlzYPc&}fp_2EYHZel6cmfAn7`s}Rq|VjFxhRAmWdRDK|K^PFnC?3HJ@- zd^;gPnp6N$(0S6&l@+5i`@8~Q9hfp)!=GXU5dctXCfcC4yFfZTM#ueKozzn>P*}V& z=O9pZddOH$fv^%Zl?SrE-Kf`|xN@yerCUF4 z+mG$=iQmP1zx6q-5Y`5;ok)nuz0zO@8-($Uxt?fDy3y0aWFUGjeFX)lz0sdINq0%J zKnwCVfW@1qS6Y$oV&zo9YRui;*mootsC{THjGk?2vfbN$ZRx52I>3lfiBwJ)0Ua{? zRZua$dv{s$Ui7=d*|f3x+om{Yp@$St`yUBaqNjzANyNpn!#$pMrzaeiizs{ccnTTm zJ&fpS^md1ISF`D`#$pg3QmRYKlF&@(wr{tJlC0OA7#eJsjacPR@n173we3~BcZ8-; zoS~s77`fP>)nOi%yH9t~J{c^6`hd;l8?%}Dg2GN?(PPoTdu~Nzy3iuaIz;!WPQ|Fy zg9;$|E-x)Ka)IR>3HE%)eet5c{^2yTxYRK}oX7*Dr2)fKM>o}*e5N;RIG||>ONH+_ zlYk1?^c9e&^Yz+3a|4OSDFK=FkPif8WxZdzbLP`r*{RH5E*rNh>4Op<*k*>56vtYv z9O*H-$yCtqR5|Aiy;MvIRhYZpA@nr7_hQoZ?n0|q(t=)ki5zg~{k06e3ZRQ>fuuxe z!vqFDQuboqV_*U<$nslVLWK(vL+|I;3POBO-b9eaic`PHdE|Q)M{R|lO7Q8isp*=? z9ea{hr@LYcu=&WuK3!XqR`hBBwSRlwQ)!C?1)nbKk$IukrUoz0sNjGk*o0kBiPz;u zUn>;Cc`96-@65jz6D5@pAzY>Y%Q&AMZ$|zgV>^rh)ns&-POdU(@R61ArM*r_d0OTB z-83YI#I-z!n0H|ZNbQJulA7-o-pDrIK)QdB{C;Zs2C{rDpo}48D{B=N3-k?(ifY&& z{(vu{!H#CtFR#110!U|)6!QF!S#}^*0jF1sL0SbX4^ud^MsS#dO#*4>61GC(EJaL87@KKX8SwN-0;LM3Dmp%WvT7saB;gb{d8 zEAVZf_fw%?)KODq-){cPh#|S0xk( zHTKN;R?cnNRL5sg#3zQVuY02U%G_p}8EeN9$YlMIoYAIvSpMir?&Vu~!9?P)=Nr%g zP%(D_&{CfqkP%PjU?L8@MsmS8L9B`UERCQSTK5`QbKo^#&68}z#Vz66%iMv-*?CKX7}=2E`AHNQ=337KJYQ%K<+59z0Rnq)!~^bI@d$ftY7wG6w^nt_ zx`$%^!}BAKBOovkATAA5S3v+cJPbWX`jv9dPh6VkaX2-C_ajKk3vInc;RQ|ZW58W} zIOn2gy*?`dRUg*F{Jj-RDDh8n5Z9SiwO}n{pkxj}5XYIA=q$bwhwrB%G`7)PxdC>* z6lx=rpsQjU7K8En-nTS`j4!ep*O;wNz+*WL$M7&u1P5Ty6PNH*+?uB0VViSp~XLu;Km#&4F3V;ZkNKfrrb@9pQ_Fcn|dY-Q`G|;c+uwUz>hX%u% zj3i&Dyn8Jyx})Yjzv*b*2d9xO6!4-8Qf*`G#%1IFoa!e1FX(`VI0K=6+ig=Egj~?M~&$BV76t0Xu2j_NSzI?IO&-d!!d+fJ1oknDh z&lSz=K6rV94#U}qnWghSE$F;o6CgWU)R7us%3Ty2{1`j3xSTDfzYRk%)0f%|@pnWs zECE6SBqoqxdB4y#h%yB5PV2ek;N&_hHIyo80TvJEFeK|0_9?a~P-v|^$ZloGXNZJf zw|k>~s-&b8@%=tzSpe0Cq(eCYn_OaO)3*!IRvZX&WOvJkq4zzl!ywQ3sY@E){MS3a zzJQ~@b%kQMzL1|P`r4mi)bfeGi0-u{Pt7SMOKD%>CmpHl4H*UR!7H7gAicrfN5rLc zO&4B#=xaZvE58Ku=HR$e6(*V1@8w0v#wp8Gv?`}>keTD+9-7eeanO%_5ZXz>sIgk5 zP8d1O4O1Djnd~kp!F-2{o`SwT)6+jIHiS&}W^%6a7~E&Fo7o}Ud(F;D zH_vpN(fW8pkFtp_>5gc2h7q$X#5C1)6{E)!EnRliSEHfMOy1cPn4J0S-qkoNixq?D zX-JGWU*qQoT;9eCy4-R)ye`q7=~ zWZZS!-JXGGI6BavGC9E>8fn|7Qf%kKDPSXqYzO8;Am=xJ#!sV?cx-6u{M>}bKB8Cd zDi~<5h2ug&XdUC7neG8LeKH6ytkb^386OMAA@c{uq81K_%EHy6QqS5?-VZ8AVMoHQ z#A9kb1w1B4??9#Ht2h!^rWy65+MUu907Bxhc3MlZ@>257QW*S*>B?fOtS@OO?_NEW zC?$F)XyH?#1DDnQLh(*e|7_*~p}FOI@G8iR0_qmLx)>aMk3Sa#RG016m#I!WAu9*? zTgruK-KvwzfTcYo1Rc1^2k&UHjg1Y7u>1s^e%3_xL~=iV?ZU(*d~-q#YS<8L z>hy@b!~?d^pbSef9xNpr)TqNC#lYt0G<|D?2^)=3;v82G@(SgfeA&U5YHu6C7{9y_ z#RdJX&QFm(dtu>Yeo{V)_%-4Rlc_kmbkh=-uGJdL$eu=c(meP@6|Ggwx+ldDJ%5fO z)yftiJg>qis0bJkAk$+aN|c|F6;HXR!3>-h?>3Xl#!W-cW)k4&D#wN-0$Ll*z|EPU z|9c=jXD=m@pfv)CGsK7@-V#{RI2gbJ*1#q3@`=N@mJB=IZUk~`&Z~RojaETGb$9J8 zD?LR0A{J%>w#t1guO-6ov61#6&2@R=wpAeVO42q9SUkJ9!>p^%5H0vP!-A=FBsfYY zRU{S`g6x`@hPVfHaThcr4aS?IfuCE}sejIoD#jrl^OWe1mliXbB&_Af@;Q~u5+%z&vC{wM;Sd+cf5kyGxl%|{?rbU~) zetKkF?|A8p`f7l(>`6OJRDIr=A*)16<~VMH+K5~MaXb8cgywr(y=P=rL`13pFessb z0tQtUKf7U?W99;Ic^r2eOqY$)*%jWxj@6nE_arjUK+^$#;?j9pZo$G*3qD_aip5l| zol+-sqPc4R6m8kLa-9$XzEfw#Yq9}*Yd|8QC>ME}2~Er=~@g0?WyA!|VlOeBc69TD)3 zUt(+tc!3QFBhcCo-lUaQat^QS>InESSgP6fX1=i&vpRS(TqHshdJ{ltu&-)SgU$EU zy^O2tp3eJ3AvhQg-^Tky)R(PdQR_Xa?2cyj9DjKKC`wD~DJO=$ja3~i8;RXa7dbwQ zHy^FLpVjvldX2<4?>}DNa?3SY=gVJuLUZ;#OA(?{O%sQ?H4|f|I{g@7kTJYAxKvu< zrVc<+@=}F2ms-Qe5y5ovfVT&3Gj+0v*8o$K$3ZsIp=wmj{dqlTvDUjbq!MpdgA)r` zx&I)4u9D*TzCiwCCsfYIJ8A7@ymsZ_+56haL<``T2ivpFb#<&SUc8t!;Bo;#kB8?) zn4MdbK(L8mbb?@4MI|@0fplhQ_6q#y@`yws(eG9A;0}hALoVzS8wM#<{PH)ZK=7pi zaogtg*oYJOLP**ix`il0)KVaL)emzQ_*QAz-zpMk2f32NPXV{k&{O|L0*1bAl$U|1 zP7Ik>ZU?)Lj?TuWribRFu*K1@1ggOEZiNu+bmS4;+9Be04g!nEi8>6NzcJD&z4dTq zx0zPJ;Wye>iw`0uwmtRn@90+}6=&GrA%T5EpMb|!k(Gs%ecpFnMUbPuyzAW{WIbo< zIpuv;v-nL}|3Q`~#l^4ag^bml#Qk3n`}+RsK>9#HJc&{r3%y++qppUjZ_|Rd=O>Jo zuy})-{ON3}2NAJ_Kzy=2)G!@M#>EDyCCS5JQx+9o;A|T#!Xep z)(RIi>=(Ouai!1YuuhWgk*$?<9E|-zc6FC1Pz;2S;(FGRjYi?2dU5;D$?NHFPR%cM z8i=mFVv%VfZJ(odC~P*$J>HRap(=kVjV*h&RL_3k)#r)5*QTT*5gAQyyE;}Y1Q7** z*tc&O8FfM~p83~woE5&2B4+k8duSX9T6b)1 zBlGc#D6|%~j05=@O1)OYSoEqlDf+Vm3&*cLRF&ejfv^Ony_Vut2n`$tPb|G#thg|p3FU5`*{!_g~<{|0qreD+UbVk;#H7{P;DHL>v|b9U(*QDlqK zs@r8H4vj%KOtkDM-`QX~`e(Dx&iKekH=&j9Zh{*NZv60iCnPusqYKod#A}h@=JlIX zF$QF51W59Y%zW)PCZeZ$RqnOC16c$}K!^(;5~pAM5=m^)3ycq_eGv7o+5Z&0LP#+ zh>pMq^)lprz7&vl0^|rHC=A`7zG3I)ifA#s48t{Xu|=7(dFNZ9vzg8kmb82S{0+QA zIk#a?5Q4!dw*!>J=?eVD8fvE?$;AN=f~a!7-`8e0HQ;J$)R}&d{}J92;}`AsNK%J< z{C2W4tZi+I$27|$fK5>yzjQ0_c_`@@gNT?_z5qW_5Dfo-K`R4SpSF+jpmQ>wAKK#W zwaAf)t3==y{D_aaRS|e2O=$4(5IN^E@2pX}C!4DN(HK%P7ibUgC2Sj&vdJ@CAr)Z9 zW8%5`-2AVwvILpSoP}Wk2cdi?!CK5I6}&faKs)!D)^mK@tjejA1w{suZwSoN68==^ zSz%t2(*anNYgZi?_s*(C9=)umAOY*8RiM?CVxgZdH2X(7yK;~|$Q35SZnbCGS-bq| zF#XqONFne$L)mfs;R_zqbXTrww!us`Zq*y;`G6l1UR#!`#h7eFsc>mDog>(9b8L9$ z>Yz&2qAKT8MNaZeYYR~+qooWNs+ghGHZfd*sU0Yy@3Z2h+xWU!j7?)NP0R(?zWPj* zs&Sc*H$hHrxaNJ}^z?MYT&WwqH?V+y5ZCAAfCgI+vcUlKSG%z_E6gi*s18xvzYozR zB)sz8V)_doTS7jG2$^7CYBDO%<8dQ@iQ9!G+mW8N5vl|!tV+^d92da1*FKr0%YR*z zVlGf@1Zs$M3v#%u7%$%AnX%hc&aYuINOJp&k+~nFB7V z>FK@!yK_xM3-F5qv+cDq9jaY1D(`inl^!MPx-|1nk7S^Tt9$NEU?6x9f-H8b8t1U$ zhonW7*N`H;XMLp>iRma1x=#k>w^V_-BI@Okl-<*QwNc)>Qy&r=2@EeGl;JXNiM<~z z$d44KkmN%-riQKtNpn(FmZL*00NYd)F*hVD<2nnJ6u?Zlw{qKAoqca>0I%u!c;>e3 zt4QtyQl}96N2%vJecA5TKHGly>Wln1oRUel#uYwF~1 z8}-bZi?{+6c|4+v6b}Z17fE=+sb%TDs@D&bD*R>OYN3q!Dv0*C!x`10hA^IY#|C z=`TOI@5HpS+$$(NH)~p3=2ii^vBqm=qsDkjrRm09D*1%F8PY$X2r1Xr5{^6W1MN@{ zlr)iH6OGHS79Q<%VuvBykOIA6NFnfJ=msR8ZKeYXbrt)xg#J2lGiCvQju3l`h_$~1 z0|Mt6HYpqOhNaod^dHZFqDFa~sIVTvq4@>T8@(Vv%ILMx?O+l697{SO*TLz6iW|a2 zn!e?ZG#hg!!lYKp@*b6P+Y%p_cyTXNw=E6{*d`|??Meu4z8`L)mQ;ITCd^F{Plsvi&QB8b#K&`dWpWTY z58A#Ofn8U27C=nTpYc}c3>G1){$HSKX?-vrj`Gnw8=>@m;P^cs@J%8$V zWhT8X=-m_oUCKl(#B_EHH49-iWB_2IDJSQ`)J(iBR%{Jw<}65O@A1qJ;W9D)-C9=1 zFWS&pY$U!HKXs3P-5J_~u;fpp493=l^znt_6ObbCTnByvY9r1GET8T8kw7oGLWm0qc!59^M^QG z;}5nu-wDHzm%sr)amu9K@H^1axODu)@ewo$sQ1{ebUnUdjEt11sFU0AcPNC^Z~8P6 zOiO?DygtXAU3vMN+We&*%CMQH3%W_4;rjFDd;SxU#Q`A)l2M>kmoOO5sO0_XFvh19 z#{KEcVoBiYPiU0|YCL}DPd8*`Wm!?40;&j}x6Tb!r5JQ)6O-znCFdPwrziX#c|9*v z#f>Ie4fd{!J#Z*updx?z{w>#~@krQG#1uNF*j5L0&3RaURF08u=}#_Luli=y*?GQ- zgtV4lZHf=T(i>hFUqs68Z-nnGjeLg6&E~7IE&WA}v#YHmXyNaIO89JT@cWhzH)G;S zDY(J|Ps004%xePura+70WnS4Vs|7Iz%3NH!v{%Ql_9M(iftP*HVvoYb9ND-^wC01+ zQNi|)(%Ik{i0b#|!@`$|)^`RLORKh3F_Zw z3Rzn|tgK6a8I@d=MO`2*{DKr!%q(IB+3dMu;q{xED>uPRb;4xYqB73}@?kF&Z=f2dN2r+U=PT@!PG~j7bz4IJJSc#@|L$@`)BQf5tdUd9cUwI%k93^r0Zy_n zFPB}Q@NRvL|F@d!lD`?YJ1{UI>+G%!{g~LIcc2V-x1mDN)m$Onx-g(dAUL6Jd^FHT zH%(xXgWv^2k7V2PHSWR_2#y4ywjh6_LqDBKC3Ksd+d=mc6d{C=Jbq8Fl-PNp^_cO` zAH8XIpZ@zJ!{2{1ehrKLZQGYWe*fQJjvPM2_FLE>cP7>?Khr(IqtosAsq%qqj(->L z*nOgYn?_slwy(eb?`gA}<-rdc?#fyUVwpV>TpYK}30TTDo4@y#djG(?mS-fCRnW3m zd|aAzzXH1~MT$(pvfPNN)sjD=o1LxJo@2vX{n#(Ywzjk!xqaOD4^_dw zl0ckyNU@R%!KPtfI1I<}D447K$Uu4{Z84uFlC#+SngWF7wRsZkex`;1t71OU?k%qE zo%5Bm!RA$Py9tkmT6J3%O#JS&^$vxh{SYf$pBPh2Ig203>2>FaLb_4;)N_kLziYO& zD3<*l?%}fgHZx4l*jx_jb6!tcU0X9XgyFzl42i54VF;W5h7->p+?QVK6~z>+ zHv1bjNmQ(ozPlEoJe+Ml85*;!@67xk_cz+kt*H|;6ji#)v+ zoxC7?{gj%)(ncSChFCuBBG#^u!r@xyk$0ZP;vy$+bxa*dYnG`zki z2!&|M$Bs>V_{@)#xi~X>9HtdMet~&waYt%B@jX3%xRS|?P-Cdm=wguW4ZN{k9v;Ud zL45^1xvb0WPI*w=p*-7zJ3B$zQ34FOTa9jjg7%C-GsGhe+Y}nH4!9gS=#-|Kx3sk6 zFJNtgw&>Wzl)EU*D++VO&DKUdWG`!50 z(B^^3l%oFOgPwxiXMy-h+GvXA)QgFeBsWlbd=QnF@C3O>)@#Tg!7SHUy1eqam(B11JW9X zrb$ZzcBi_u&&c}@^_=so%7X10+HT%$BKpmGt<&4UYD3engqs6n-9UZ$KCey|KS zWDJx2m47Xui!9V%7(R??Mn`&mHFthsb|OK%g_+M-?Kq}cx}IC(4rK$eX2EGTx3nDT zISN%*5Q2{Mz;5iyG7V2iNJ#7{e{*WEa62tq?5VTU*%m0qvj5m_2nP#XX<>#B8^4KLL{a>VDo2?zNDB ziIosK(N{vyhr_aGo|q=!ai^|!Xufj zDz9Jfiu-UdWQxBktOeN`s6w=K?j9`S#q>`0^frSWtM)gyAuXpVe`Mkahlli@BLWki z&3ow57jWcME=`@?rYS`0HY2%#@9g#HB}^2YouF;EoA(#emD$T6c#U#$Mx;o#khprI zlat@_wxvcw-?5VSmOb*?^Uy`n4TdOcAGA_6Snb;tK9Nx%maPgiB0Zl&{SL*|ocQhQ zE3#EYYv=frraT;gN9-d#@IOzQGoFZj6PV7F!!0yca8E8p@|1iDS=UGgewcU__7d-e z7XbC2Za$s@zh=P1m@dr9(3^dGKL&dj`{ymWycZYXwBVa9KAO_1EOFk4a*RVy)QA+R zD6TCu&d!Kq6z=Gz)YSMdueQ>GqjCHa28Z~i@t_)bpZeKKS$uUu>)Kj44Et1T{%Z$j zUVTyBvm{`8`hQ}`CaA9@ruk_W4@`}m-e<*nq{m9VYbF?Xdcv=Ah*n`t{n!-~Q z_73K22Z%ukxv;SCy$2FZwVyUM?$z|W1s>7nL4WD}fI~S2 zE7eI=6)a`G{MaBHNU7BQfxaIuZMJDQrvi)KGk$;M$z1rDhT|LgBTJGM9clNlN72RQ zj2187@K63?TzmjCKd~{ITT{#vx zpA?u6Fs9V!j9IDToLz(?4WjOcTfe*ST^F6$8$dljkAX`*1TLitCV(Em^i;lckAUIh7FPeI=3;W%9;Mz77!X_lOG;l^2{Ro9OTPU2DB95@Gk*_s2oIC{m!C} z|Msq8HPGCm6pDF7AXd{LLndw~)ONyMcPQMm6boX?Ol*TF@dQWxiN1f@(7|-lxI-st zvBx$Fbx$E)g(rxQ2f)rD@DI30R;nlCB(sj&zPGnH35Qtwtk2RC32EB5s9-0?UNyTO zxmEl1MSThk`gADOtqr?{9QhZ@9@N&PAV!YTRi6uk<2sFM8!m?{xbNFcD5@9C*NBqs zPMh4KjUdixEsRyt($aF;-6ZSyAT)x{+n(ynR;32BR9|pg!mKoZUSqWnVvh&+Ei9It zv7M%uDCANO76l8F!fbO6`W1NTlNQZU>+=t$F!?9;d+a1{(nZ+7mg%c3-$-`QY)j|?{5W}E z=Y!zmQUx1RTZd=!2lUtg*XRMRF*S5!K$JsXmU6i=pgX7~f7r8=S*%sjPKH+dt!BQ{NPu^4;;4CYij(UIOpbB9X z042qZ6rLE;yRzOc2ot*GbVE#QFUG>5U6+r3=yE{4tn_7)x_TFWO?aYpFFb#+cyf70 zstarihXnV~m5vkarvTTtn3>Dr4BMuW{-B1zt_Ta&dU4VLfh~?q*A&VuIqwI&Uj<7m9C>pE5u$6?BNUUXYM#}@GNON zauckDob8rUI911+DBh&6{oxN_=9WO$N0K*5vZ)XAxBj{totfl)2{2WvZSk#?*CNRX zD1SmI966JjhFF!P-|%aqDlRzy0Y_B|gcgHtnB|X4Cyl(*DrIMRe`re4;2pAno7cX= z_w_Tk(JmUU4U)g3>pGR;2`peXg4Tt5S^;*rb*VZZ%_`boyK)s-zmBM2m#a!jNs&2g zwF#INV_DgzmiUVXyregzc`g|zQvYEJy z2kE3c?|V_1%tk!Pl@OVPWn}Q+P+Y^>p{jlRAhQPjGEOp>9nfH~;2V0&`@J#b0okBf zaUzR+H1}y176C@LZwc!C*hAX+s}6KBR?+yr;5G9v=g`j(b}2sfI`2!zE7 zmT={m!(7KnZ|C^6`Dfl!GI27dhJ~dHR*hoDML-7{T#r6fh9AA1wD|Gv7&&In{a*xj zja07O8k?SG|A=Z+@9fgvXK!zB+$hF#xI*uMEUvql{<1!2YAHy?yLqBMM-KwKaThQWa=0d`B#YSrglm(g z=@S@a;?5gUOi;vc=d z#g;5f#}?y>xQx<(tGYi=q-A83L9MTX9BrG`1v<8ux(}|gf*Vd$HC|d@p#;W&=YBAP z8dQK##BzRRq^wAILnj&ka&~a=Z{7G;<1_sf5@hcagdy~T*s2B~f*#C(9C?dTj zaZ%6xNlk!-+dz|i{&>NO*pCd#ma`?CJw={dYwx&o7?BDJqSyjq2M{BdoJx5T5$qM% z51zDVB$xq3XlSh|okCop15jZ?F@1V$EdAsgKz|8va}ETzmofo%<{eAWFWVWoPj?#4un5!V zgxEZ#?D$9`{zae#-;#YsbYNKA{yct$nQ%ZpFWBY*dwRQJfmO*`4_!`v|Gs~kynvvT z`qXS^kjbpt+V0VrGM&znm1-_oY}tfttab->7l|OFu@a)-=h`6*bo85EAD8uBj5#Gz{0jC7pdoKE z+-*AqaYxE1mFIBa=`y<6v?rItm=eEn*BA;fMq90~PIw!%^_wTS%GwWeQJK6w5-@PFOwrjY{q({2lD=)}^MQc0wEaY=D9F|g&RGU9HnQ~{D zMEsf)WM`5)kcV}j-NihacY^esUKnd#iJQh(ad3Q;ikDHZ<*H>v=I(WdWHdrH^yrmT z#m=x;tFdUKCzp2K#_u9nF5g?Qei%EncpNHtf-oVUJRtT;X>Q&W22LR4v7zbrS z#ZbiZv;Kqk*P44PE|m++rG%8|L#IWvqTx<`Iu;dMeJap$<42=M1^z_5nkQzfxN6-> zK=i9u)aF+p;E})3aL&4bgyF7iLp#ysji$W7>U>BU!mNmSL+hxjXvnyhJII)Xik1LAQY)(T!7Q%og-obqu$KvwOhe|A(1|NjugjwKX9?xM(_~! zs{q9y!U7Z@94P6lnZ*NJ^1x;k0!=G`gPCRw%>wQR3NKkx(cF{L;#UXv=>Jq}h+?co zvho3jeq9iO>t*J7Z>fx+e{__dN#|sS3UMzg_0BjELlBa$yuQbjJ3xjT zyG!_GdGZYfwAL@o;1>soDW)AemEL|Z;mCrB9M-iA!6eiDoP&xK+!+ zpy;zS@IBvWSV-rBMnMdN!A!f~i}14Dj&A_tio)P9%drLxh9!T_Q~TG;Z&P4D_f?48 z_U_!dQ*CyjB+C+7;g}fGRAayFVs?A6hwey65Y(%F5bRq)@h@(vEj)2VMj%F#wI-W@ z*JikUx|_@cTD|!fZm!d)95NY)F!hqQz)T2%KEq*{t729ErgP~6tQjKcD4d$u#j4~k z283SXK3lc}^b5hWIuND|0TaKrsX3bl8IY1~Fz5;eA&ADRC1HOWAQ0r`eM^wry`u|R zDfg-U!nv;@BTR#7L4n6ner^#r#$$+OC9KbWmQL!&V0v%` zA)Sdfiy^{z*90ucX8Og+AM1)2g_IVKCgy(Dhvh7K?NRP$xzB3KGJi0hMDB)0LyF`{ zqSfrc(W6Jp^HAkJcohS1$p)GIPB#MACR4=S7^jgIY%Br}DQ7h7D5maribd6XN(A+wdegqTJx!r&9!p@# z2*Nm^kZv(ZEE3qLX=vYgdXMxN4*tkyxF*Oc9dN#I+0r z^yC6qQ-V=QfSW-We~TalCO1iSaeP!U?;6#bh9^X^tlHRwr9Y`oap(d9vk5;iD7}!NAxtE#itdl4GycZ@RR@t*7+q}ND@u0n+poC(|u)Odz z5MB9Kb;FkEn1x$~y${b2_L27b^%x?>N@QhTzRbjVi4H7rU^Wvx_d~8;5OHJhBg32P z%3uq*z&+RGaY6~?Q5fR|#?X3kaj^vEJ3Z8%4}HFWu2<>O%dWY1wZ-Z%EzhiINh-4V zI}C=RvW3zAwuOm0K2<|aP%d#ITEWopn?V2Ha#Dpi+k>kf0M>B^0-B6)t_KrAF(AUa zpJfD>&`V%Ik;^D{ zN=#78yG_lRaljGkTF=5{X-KY%Qw}q?3xBxIEl(v&r3H3*$ve8WXbU2T!vUjR(Mis- z=XeM`kb65B($aLp0cHUM;|@!=(O4TYWr|s~#u#D=1PR3qb^w{`z@Z+1Zi`jQq;p&j zs`)K~rmh1$-ETocf;(4Nz4Un({@SDF2Sv_gVx(Mg%QXkDf(N_1%~IHsn4S-4h3I&| zSJ8kqmCt(aySAYrN9t~Y9BH7|AXF61{-8fv$6MANK>D3mc9Yru%`YALgfgC0Lnz~U zNp55MuRR0LV1%7KX1OiN`0Lfy;@8#OqGX!rgdy@TxmJ4&b2;NNtvPBd&`moy`L0{L zZUXP9Ibv4zI^RtNjA=}oZ-UA?|+k7B6KI$pCNmM4l}sx z!pAAg(>a55F!bH5NS81u95(;@CM7d`?k_||F(e3G`lW!dlrdN!H2Vs9y2T(ry>B1y z&Q{LXdMZ5--~`FRI3MX_&Uir+KtM4fT>b)PLq3}-1Hps62S`*s@h)u0G1l+8t5YmO z2Ry$8_{0{-YiR`;RbQ20$ZE6JvUlW(&8tc6t zaw4Yge_VL!7!SZw|2$S$nu(pO+B4?Xp%Zl!GxLQQ+LKlc*iuUBH8psRy+8oJ22Frl zKoxWZ!6IOy#K{LZwEeAM$ou2w(3`Vol$bKS5_Jo*7@J|zyUQgh#~%NZxd>L^jeLri zLEH4pQv)*`_N0|0zAJ)Fx}7_>KCj*iIU30uB}a4L0@k#eUv^nhRzSa45g=2ol->i6sA-qvrU5w%^Z8q?ACKe z6fH=4hdqw>J1ht!tt?~^8b_C7q*b`vo6V8qJO<)1seJ;hUTYN zaXaJG7r(yXod5fv8gPB>PB(?--?c4%_x2`dD2HcVNqO&{w zL9_`^1{wxaaMjPqi>$|?cr?hg)*YDByMjY!+D-hF2Be{GZ_=caRl-15l;Vm_wzBlq zk0H87>G>C_*Pz<-=v8Ai#PUSGUI;%>;I7) zw9@d!{E)jG-|;B-(WbS${*+e5-h90k<`TxL#LEmW$0T?_KKnIgRfs9{b$LYHpPj3Ol=Ot1Zy-Zcic8RZBn7E!m4x6$l zG>>OCQSbD_CkM;N7ng^|qRPB%r|+h#25#Uy!`buOaA!%O@5N|@u8i29=bHUf*b-0$ z4ZsegIU&?az;=7XjBaG~J~DW0REVp^C|OW}w>g;~s3~eT{%tVKFJCTkqrb~@KX2x1 z+fv<4Z?dxJP6^Tm2`kw2+Rw(M)OxUhGIqA-)FnE8-Z4X1Vxy??R;M3E0NhXdMjQ@AMJw5f|;EPQ-y>=ld<>f+zRSTIB< zqZo?C_e|`A2_5?d7wW#+6!vCMLGp8jOY3ZR9iO$y@!94Cs$BK0=pW7s5m{_0M1MvZ z&Z~`4w?Uu)!UKU}f8I$^sP7|rQ|-gZf;?~b&gedZQrc=`dAP{nxq-L$lFHoZXFu0E zaElle*pHK=me+2;>mS||pT}(jQr3@YBX0CWeu%sjN7pG}b9m{yenv?e%#>)@cU#Hi zuA6Y5BgKCxJmU|qxJiYpOp){uO%i&+%_1Ir9C~nr!4OsiPchr+>2PJtslQY2U=$o_ zD#!8WM>Mg?kBNb9Lj=eeb3=@+8B|^*?+eFuae1ut(}s7o_>zr9;p09O6uPWJ#1RPW zaOS+Oifef?+7c+(Lr%91#I;i5q}}iMJ^81aZG6y+(};LJ=(%mo?(?pW5_v}QD(rQj zAgg(WJZ%XSoE~jU1i_E2!)jwvCqu74IjFxRN5pE=Zf9wUc{|_=ftX>(?XMGnayZ3u z`5s|<(6SykD%|#B(Xk+XI67*;C0fec;2bUeuZD_=Cn%ueoWc*F!VAJSk^;`fV$HAM zI8MdEIvJ1Bz^k$HoMx^UDY}uPY?Nd29Rz!x6FvAPxwOZ>kF#=AEzC>5f_dEB5-*Ft6rX{N=YkU|LH*u z>B$iv&Y^RN+#53C!$Ht+NCzJ)4|z3*EAj=vMHxdh5`OlR-;#MrS9t-nsucAKH62Ha51j zX#wIg31w+o!Sv$2_Lj-})K&0vyyIS?Vc*w&A>H(EU*FeXi3DB&rcIKR1lC<}BwnoN zt+G%+H@@Qx2x}ni=>_soTmf^qWiJ)zzVpT1pS(BGUv|KFh%6pNN<*l2uF?yp2rREC z_WUZ*x7|Hhr!u2}v6IokitOMEAA27*@`dM2b0V!QK8HQt9u!Psva+x%QTqJGHO1*%Q#byZ!har2I3S`Iha!WEs!UnZWt!hY%uP5_3hfwTe;b~n|FC~ z*CE>?*t0%Nu{kGvek4ICar>Wtr-=(Q%8CabK?|whhGiyjE;)eEhYa<0o+1(;EocL8 z)Hx3XLklSIrJ6XHfwX^ZfDWnQ#RMULvw7{jIufR1fzs^p(2V(pf^Cq44aAoJ|3%0W zn1OnLI2Ztw?9f^F!5CV6Pahx|Ypb1>B8@Mp)OE*z%%B3={+B)g(7v648M-iA=OYXL z!npk8Yyeb_Qw+qNXfGSm-4Xx1eqk2H;%&Se4BW-B2gThW^&YHOwEs5aVI=XR5ChMp zQRWrpqhH@>t>!7?fd0=iLUVUu542@&r*%68g`uOo%Y98!r1uX!$qQMzK->nFuBBrz zweg=2yY48JqnQ0Zm4QyeJF_AQI+f#twAap&@7eK7IePF!2F&N)8E9w_2W1I4O8q>w4U;;g&bkT zzKs8y_DK8FjBd3KPt5Tlml%4|X9;E{B?WGcg9gb!h_IfyQKjN>t}m}9G8HKr@bL6K zVH^A-46nO{m`@axpi`iL^lGOp+ddF^Y|>lPxQ7=M2yYgb{BAmmQne62gX3mFsal}} zNa{)T802LN=ZCuKt0P;8u{cGbpa*ztOQ!|R(bNO=(^*q@{}Br<0ONV*%0DtxHa`uT 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 SVZRwPtF-eq`a3>+hktMC6v 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 z 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 z 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

eh#xA!pu~x*VkJ%MBl@eIIq{ zHA1V|2kv({fF{{_GpgqZBq)tW+-;;<7ik2t;$_Q+`y8_gj8b}BVXVFnJ9P3_I7m|Vr^p}aJ0~GhaCs1#`*2fkhEm35H_g2 z0*53Bkt5NfPpkGJU}vJsYL&B4Etv8b`7=8rE@O3qM*GvM+Us$>aX|x+Yv{Xm47^t; zj&!<9?qqAS&I*&|Sfu5G;YRKPDy4Wk8HgN1{I|mgp!J+06Kae5x4tf$5sUWpY2!3N zM6rjv2ulLjFcz6n!c49<^}% zt-AaAtd=!y@NJi4(&gJ>t_a%+Oe6H!bymOx?)p;ugt$FE{&rYBVsrAoksEd2E!@+n z3VAq)(1uMKjLE;w(+4?+942H@vla_++>|-$6@n=aWX-Z;N2l2bAt#|(Gtp4a`yN_` zqbVo(sD;DF__(4|zS~G`&sYR%&8^6tdR10d%9dW;j&iv4E3?blN@Njoe>E~jE5;{H%vwAO8$ zmIu@sV=S+~9X1(-S&1uDF3i97Q;Jnwh3R!b-H}*XPNTir!hyd8sE|=@$rTq0=0HCAwn=jg3k)W zqVKv=XAggO$<e0JtO!FRRB?*aqO$?F!VT#+=js6*Ll|S=VziS^da~zrs85f z%2-{j)CL@{xc+y3TlQHGbKP3m|L`CG zKd}MoQOH+J2`c?CKtNXO7Z4M{l=C0e{!UnGwSk2ldJMy2d}gp3Ow}A;NSRk|NNMr zi5lJipFjCG|8dt`u5j^h{(8s9*M-RSAv%8YZ~m+Q|D~|;%@VzK;omILn Date: Fri, 4 Nov 2022 14:30:19 +0100 Subject: [PATCH 167/208] fixed unit tests --- bitorch/quantizations/dorefa.py | 6 ++---- tests/quantizations/test_quantizations.py | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/bitorch/quantizations/dorefa.py b/bitorch/quantizations/dorefa.py index cd2b6a2..54cc314 100644 --- a/bitorch/quantizations/dorefa.py +++ b/bitorch/quantizations/dorefa.py @@ -12,9 +12,8 @@ class WeightDoReFaFunction(Function): @staticmethod @typing.no_type_check def forward( - ctx: torch.autograd.function.BackwardCFunction, - input_tensor: torch.Tensor, - maximum_bit_value: int) -> torch.Tensor: + ctx: torch.autograd.function.BackwardCFunction, input_tensor: torch.Tensor, maximum_bit_value: int + ) -> torch.Tensor: """quantizes input tensor and forwards it. Args: @@ -76,7 +75,6 @@ def quantize(self, x: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: DoReFaed tensor x """ - return WeightDoReFaFunction.apply(x, self._max_value) diff --git a/tests/quantizations/test_quantizations.py b/tests/quantizations/test_quantizations.py index e71ac77..7333b33 100644 --- a/tests/quantizations/test_quantizations.py +++ b/tests/quantizations/test_quantizations.py @@ -59,8 +59,8 @@ SteHeaviside(), 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], - [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], ), ( SwishSign(5.0), @@ -81,7 +81,7 @@ 2, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ( InputDoReFa(bits=1), @@ -95,7 +95,7 @@ 1, [-1.5, -1.0, -0.3, 0.0, 0.3, 1.0, 1.5], [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], ), ] From 85ebf400fd9643c7d40cce2743b725788667322f Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 4 Nov 2022 15:58:22 +0100 Subject: [PATCH 168/208] changes for paper --- .../notebooks/Quantization_Visualiztion.ipynb | 36 ++++++++++++------ examples/notebooks/quantization_functions.png | Bin 1264988 -> 0 bytes 2 files changed, 25 insertions(+), 11 deletions(-) delete mode 100644 examples/notebooks/quantization_functions.png diff --git a/examples/notebooks/Quantization_Visualiztion.ipynb b/examples/notebooks/Quantization_Visualiztion.ipynb index 53741fb..e88609a 100644 --- a/examples/notebooks/Quantization_Visualiztion.ipynb +++ b/examples/notebooks/Quantization_Visualiztion.ipynb @@ -2,17 +2,19 @@ "cells": [ { "cell_type": "code", - "execution_count": 4, + "execution_count": 40, "metadata": {}, "outputs": [ { "data": { - "image/png": "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yqJ+/foMGDDA4/MWFxeTlpYGVL6wPWi5ELvTq2YhJYWFzLvsMrL276dBp04kzpxJiBvbNPuTEl7Ed4qys/n+jjsAOOv++0mowcW7OsphEes69uuvlOTlERYTQ6MzzzzlcY34Eil1usLXgcWLWXTvvQAMe+45Oo8fb0ZoIlIDRfn5/LfCDZ/Lvv2W2CqKUC55eXksX74cgBEjRlS67l9VkpOTMQyDevXqERsbW+lxugbbm141izAMgwV33smhFSuIqF+fCfPnE9WokdlhncKV8CLifcsef5zsAweo3749Q555xifn0DBtEetKXbsWgGZ9+xJ8mrvcyl+RUicvbn981y7mXXEFhsNB12uvZcCf/2xmeCLioXfatcNZVATAOU8/TftRo9x63tKlSyksLKR58+b07NmzRueuOM2xquurPgfbmwpfFrHmpZfYOm0aQcHBJM6cSeMuXcwO6bQ0WkTEN35bvrx8esaFb79NeEyMT86jHBaxLlfh63TTHEH5K+JSccRXQUYGsxMTKTh+nISBAxn97rsqDovYyMdDhpBfNtWwwyWXcM7//Z9bzzt+/DirV68GYOTIkTXOe3fW9wJdg+1Or5oF7P7qK3589FEAhr/8Mu0uvNDkiCqnIZ4i3leUnc03N9wAhkGPm2/26S6uumiLWFfqmjVA5YUvjfgSKeUqfIUGBfHlpEkc+/VXYlu1YvycOYRGRpocnYi4a9H993P4p58AiGvXjsu+/NLt5y5evBin00nHjh3p2LFjjWNwFb6qWt8LdA22O++umiweS9+6la+uvhoMg163307f++4zO6QqaWFdEe9b8tBDZO7dS1zbtgx/5RWfnkuL24tYU3UL24PyV8TFVfg6+MYbHPruO0Kjoxk/bx4xzZubHJmIuGv7jBms/89/AAiNiuLWpCS3n3v48GG2bNkClI72qqmCggKOHj0KuD/iS9dge9ItfxPlpaczOzGRouxsWp9/Phe89prlE0kjvkS8a/f8+Wx+910ICmLMhx8SERfn0/Mph0WsqbqF7UH5K+JSUlLCAODQjBkAXPzRR8T37WtuUCLitqPbt5cO/gAIDubmHTvc2sERytbGXrAAgF69etG8FgVv12ivBg0aEB0dXeWxugbbm141kziKiph3+eVk7t1Lg44dGfv555bbwfF0NMRTxHty09L47rbbAOj/8MO0Pu88n59T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gQM4HA42bNgAQKdOnahXr565wfnQQw89xI033kj//v0ZMGAAr7zyCrm5udx8881mh+Z3OTk5JCUllX+/d+9eNmzYQKNGjWjTpo2JkYk7lMN1O4eVv/am/K3b+QvKYbtTDtftHFb+2ltdzV9QDrvYNofN3lbSjm688UYDOOVryZIlZofmc6+++qrRpk0bIzw83BgwYICxcuVKs0MyxZIlS077HrjxxhvNDk3coByu2zms/LU35W/dzl/DUA7bnXK4buew8tfe6nL+GoZy2DDsm8NBhmEY3i6miYiIiIiIiIiImE27OoqIiIiIiIiISEBS4UtERERERERERAKSCl8iIiIiIiIiIhKQVPgSEREREREREZGApMKXiIiIiIiIiIgEJBW+REREREREREQkIKnwJSIiIiIiIiIiAUmFLxERERERERERCUgqfImIiIiIiIiISEBS4UtERERERERERAKSCl8iIiIiIiIiIhKQVPgSEREREREREZGApMKXiIiIiIiIiIgEJBW+REREREREREQkIKnwJSIiIiIiIiIiAUmFLxERERERERERCUgqfImIiIiIiIiISEBS4UtERERERERERAJSqNkBuMPpdHL48GFiY2MJCgoyOxwRSzEMg+zsbFq0aEFwsPVq2cpfkaoph0XsS/krYm/KYRH78iR/bVH4Onz4MK1btzY7DBFLO3jwIK1atTI7jFMof0XcoxwWsS/lr4i9KYdF7Mud/LVF4Ss2NhYo/R+Ki4szOZq6zel0smPHDgC6dOliyTsjdU1WVhatW7cuzxOrUf5ah/LXmpTD4i7lsPUof8Vdyl9rUg6Lu4qKikhKSgKgU6dOhIeHmxyReJK/tih8uYZ1xsXFKeFN5nQ6qVevHlD6euiibR1WHf6s/LUO5a+1KYelOsph61L+SnWUv9amHJbqFBUVnZDDKnxZhzv5q7+4IiIiIiIiIiISkFT4EhERERERERGRgKTCl4hUacqUKZx99tnExsbSrFkzxo8fz6+//mp2WCIiIiIiIiLV8rjwtXTpUhITE2nRogVBQUHMmTOn2uf88MMPnHXWWURERNCpUyemTZtWg1BFxAw//vgj99xzDytXrmTBggUUFxdz4YUXkpuba3ZoIiIitqE+tIi9KYdF7Mvjwldubi69e/dm6tSpbh2/d+9eLrnkEoYPH86GDRt44IEHuO222/juu+88DlZE/O/bb7/lpptuonv37vTu3Ztp06Zx4MAB1q5de9rjCwsLycrKOuFLRESkrlMfWsTelMMi9uXxro5jxoxhzJgxbh//5ptv0r59e1566SUAunbtyvLly3n55ZcZPXq0p6c3VV5eHvv27TM7DFM5nU52795d/n1d35GmLm5lm5mZCUCjRo1O+/iUKVN46qmn/BmSeCA3N5eUlBTatWtXvjONiNjH4cOHCQ4Oplu3bmaHIh6qy31ogIMHD5KdnW12GKZRH/pEcXFxtGrVyuwwPFKXczglJYXPP//c7DBM5XA4SEtLA6BZs2aEhISYHJG5rrjiCpo3b252GG7zuPDlqZ9//pmRI0ee8LPRo0fzwAMPVPqcwsJCCgsLy7+3woiR4uJiunTpwsGDB80ORSxk8ODB/PTTT2aH4TdOp5MHHniAIUOG0KNHj9MeM3nyZB566KHy77OysmjdurW/QpQqZGVlcfHFF3Ps2DE6derE9u3bCQ31+WVARLzA4XBw++238/777wNw//338/LLL7u1hbfYU6D0oQE++eQTrrnmGrPDEIv57LPPuOKKK8wOw2cCJYcdDgctWrTAMAyzQxELue+++ygpKbFNAdDnn3hSUlKIj48/4Wfx8fFkZWWRn59PVFTUKc+x4oiRjIyM8qJXo0aN6nRH0+FwANjmTe4LDoeDjIwMNm3aZHYofnXPPfewZcsWli9fXukxERERRERE+DEqcdfs2bM5duwYAElJSSxatMh2dxxF6qpnn322vOgF8O9//5v27dtz//33mxiV+FKg9KEBNm7cCJT2EeryaGP1oUvl5ORQWFjIpk2bArrwFSg5nJ+fr6KXnFZmZmals4CsxpK3+q04YqRish89etTESMzldDrZtm0bAN26dauzw7T37t1Lhw4d6tRF4N577+XLL79k6dKlthuaLqU+/fTTE77/5JNPVPgSsYGjR4/y4osvAvD000+TnZ3Niy++yN///nduvfXWOl1IkBNZsQ8Nv/ej77nnnvJpX3WN+tC/u++++3jttdfqVD/aXVbMYVfBFmD9+vX06dPHvGBMVFRUxM6dOwE444wz6txyNy5btmyhZ8+ewInvDavz+V/c5s2bk5qaesLPUlNTiYuLO22VG0rvBsXFxZ3wZTb9YZaKXCP+6sL7wjAM7r33XmbPns3ixYtp37692SFJDRw5coRFixYBpR+coXQEWEFBgZlhiYgbXn31VXJycujduzfjxo3jmmuuoWPHjqSnp/POO++YHZ74SKD0oeH3/lJdnjEhv6sr/ehAyeGKxY26XLCVUhVHrKrwVcHgwYPLP2y5LFiwgMGDB/v61F7ldDoBJbuUcr0PXO+LQHbPPfcwffp0Pv74Y2JjY0lJSSElJYX8/HyzQxMPfP755zgcDrp37864ceNo1aoVWVlZfPPNN2aHJiJVMAyD6dOnA/DII48QHBxMaGgoDz74IED5YxJ4AqUPDepHy4nqSj86UHK4qKio/N9hYWEmRiJWUHGkW8X3htV5fPXJyclhw4YNbNiwASid8rVhwwYOHDgAlA7PvOGGG8qPv/POO9mzZw+PPvooO3bs4PXXX2fmzJnlHTa70J0qqaiu3KkCeOONN8jMzOT8888nISGh/GvGjBlmhyYe+OSTT4DSHYmCg4O56qqrTvi5iFjTunXr2L17N1FRUYwdO7b85xMnTiQkJIR169aRlJRkYoTirrrahwb1o+VEdu1H19Ucrjiqp66vTycn3sAI6BFfa9asoW/fvvTt2xeAhx56iL59+/LEE08AkJycXJ78AO3bt+err75iwYIF9O7dm5deeol3333XduvK6IItFdn1gl0ThmGc9uumm24yOzRx08GDB1m2bBlBQUFcdNFFAEyaNAmA+fPn1+nt5UWs7rPPPgMgMTHxhLW8mjRpUr5bmOsYsba62ocG9aPlRHbtR9fVHFbhSyqq+B6w06hNjxe3P//886v8IzVt2rTTPmf9+vWenspSNERbKqorQ7QlMLhG55177rnluwv17duXM844g507dzJ37lyuu+46M0MUkUp8//33AIwbN+6Ux8aNG8d3333H999/z+TJk/0dmniorvahQf1oOZFd+9F1NYeLi4vL/62pjlJnpjrWVbpTJRXZ9U6V1E2u6YxXXnll+c+CgoLKR31puqOINR09erR8Ss2IESNOefyCCy4A4KeffiIvL8+foYl4RP1oqUj9aHvRiC+pSIvbBzhdsKUiXbDFLnbu3Mm6desIDQ3liiuuOOGxq6++GigdUXL06FEzwhORKixZsgTDMOjevTvNmzc/5fHOnTvTqlUrioqKWLFihQkRirhH/WipSP1oe1HhSyqy61RHFb7cpCHaUpHrfaALtlidazTXqFGjaNKkyQmPdenShT59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kvSjH/1I11xzTUhfzyZDhw7VqFGjNHv2bN11111auXJlyHfRHj9+XHPnztXbb7+txYsX69SpU75/a9u2rYYPH67hw4fr2muvpdDlIrQ6IhDm0faxsb0NoWHjscAIFAR2fCEQVqrs47Q5jhkzhusURKmkpCQNGzZM0pnjAe4SHx+vwYMH609/+pN27dql9evX66mnnlK3bt1UWlqqefPm6fvf/74aN26s8ePHa968eZV2+9SU8vJy3XPPPdq6datatGihZ555psZfw3YvvviiGjVqpHXr1unBBx8MyXh46tQpLV68WHfeeadSU1P1/e9/X++//75OnTqlHj166He/+502bdqk7du36/nnn9eNN95I0ctl2PGFQJhH24fiNRw2Hgv2RWwAhS8EwoBtl5MnT2rOnDmSaHOMds7nP2PGDPLX5Twej7p27apf//rX+vLLL7Vx40Y98sgjatu2rY4fP64ZM2ZoxIgRatq0qX7yk5/ok08+qZHdA16vV7/85S+VmZmpWrVq6Z133lGDBg1q4DeKLE2bNtVbb70lj8ejv/71r3ryySdrJKe8Xq9Wr16tn//852revLmGDBmiN998U0VFRWrTpo1+/etfa9OmTcrOztavfvUrderUifmZi1H4QiDMo+1jY3sbQqNi4ausrMxgJMG7oMLX9OnT1bp1a8XHxys9PV2rV68+5/Nnzpypjh07Kj4+Xl27dtWCBQsuKFhTaHVEING2Rbu6ee82CxcuVGFhoVq2bKn+/fubDgcGDRs2THXr1tXu3bu1cuVK0+GgGtLS0vSb3/xG27dv1+eff66HHnpITZo0UX5+vl599VVdd911atOmjaZMmaINGzZc0GucPHlSd999t5577jlJ0t/+9jdaHM9h8ODB+uMf/yhJevzxx/XQQw+ptLT0gv5fGzdu1KOPPqr27dsrPT1dL7zwgnJzc9WwYUPdf//9+vTTT7Vjxw499dRT57yrqNtF6zyaOTQqsnUeHW35WxHdEnBU3FkdsYWvzMxMTZ48WY899pjWrl2r7t27a/DgwWfdQcfx2Wefafz48br77rv1xRdfKCMjQxkZGfrqq68uOvhwYccXAommlarq5r0bvfbaa5KkcePGMfmOcgkJCcrIyJB05riAXTwej/r06aPnnntO3333nZYsWaK77rpLdevW1Z49e/Tb3/5W3bp1U7du3fT000/rP//5z3knZl6vV4sXL1bPnj31+uuvy+Px6M9//rPvIvyo2oMPPqhnn31WkvTCCy8oPT1dy5cvP+/4eOrUKa1atUpPPvmkunbtqi5duuipp57S9u3blZCQoPHjx2v+/Pnav3+/pk+frv79+1s/D4vmebTtnx1qlo3z6GjM34pjJzu+4LBxx5fHW82/Nunp6erdu7f+9Kc/STpdpW/ZsqUefPBBPfzww2c9f+zYsTp27Jjmz5/ve6xv37668sor9corrwT1moWFhapXr57mzp0b8gvZBlJQUKBRo0ZJkkpLS6P6pLm8vFybN2+WJHXs2DGq34sdO3aoffv2io+Pr3R8h9uxY8c0cuRIFRQUKCkpKSSvUd28r8h0/kqnc3j06NHyer3avHmz2rVrZyQO08jfM1asWKEBAwYoISFBs2fPNnpNoHDk8MVwctit8VV04sQJzZ8/X2+//bYWLFhQaedRgwYNdO2116pbt2664oorlJycrJiYGOXm5mrt2rVasGCBvvnmG0lSSkqK3nzzTQ0ePNjUrxJQeXm5Nm3aJOn07je35XBWVpYmTpyoI0eOSJI6dOigoUOHqnv37mrcuLHKysqUn5+vrVu3av369frkk09UUFDg+/natWtr6NChGjt2rIYPH666desa+k2CV938CPc82g1j8N/+9je98847+tnPfubbSRmNGIMre+CBB/TKK6/ojjvu0IQJE4zFUZ0x2OR58F133WVkrnLq1CnfIuHHH3+s9PT0sMfgFiUlJdq1a5ckqXXr1lF9Pcns7GxfB83EiRONXey+pKREb7zxRlD5W639iiUlJcrOztaUKVN8j8XExGjQoEFVtousXLlSkydPrvTY4MGDlZWVVeXrFBcXq7i42Pd9YWGhJGnkyJHVCbfGeTweff3116xY/ZczeEerffv2STrdFjNo0CDD0YROdfPerfkrSf3791dpaanvxDGaRXv+Jicnq3379tq6datuvvlm0+GghiQkJGjMmDEaM2aMDh8+rFmzZun999/XsmXLlJ+fr7lz52ru3LlV/nxiYqLuvfdePfroo1zT6wJkZGSof//+euyxx/T6669ry5Yt2rJlyzl/Jjk5WTfccINuueUWZWRkqH79+uEJ1oBwzKPdPAYfOXKE8fe/on0Mls4cm2+++abefPNNw9Gcn+nz4DfeeOPCg68h+/fv19at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", + "image/png": "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\n", "text/plain": [ - "

" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], @@ -27,6 +29,8 @@ "import torch\n", "\n", "\n", + "LABEL_POS = -0.4\n", + "\n", "def calculate_numeric_gradient(x_values: np.array, y_values: np.array, x_step_size: float):\n", " # :param x_values: a list of equidistant x values\n", " # :param y_values: the list of corresponding y values for an unknown function\n", @@ -69,7 +73,7 @@ " x_values = np.linspace(*xlim, steps)\n", " x_step_size = (xlim[1] - xlim[0]) / steps\n", " \n", - " fig, axes = plt.subplots(2, len(functions_to_plot) + 1, figsize=(15, len(functions_to_plot) * 1.5), subplot_kw={\"xlim\": xlim})\n", + " fig, axes = plt.subplots(2, len(functions_to_plot) + 1, figsize=FIGSIZE, subplot_kw={\"xlim\": xlim})\n", " \n", " if len(functions_to_plot) == 1:\n", " axes = [axes]\n", @@ -90,7 +94,7 @@ " axes[0][axis_idx].plot(x_values, y_values, color=\"black\")\n", " axes[1][axis_idx].plot(x_values, x.grad, color=\"black\")\n", " axes[0][axis_idx].plot(x_values, numeric_grad_function, color=\"darkred\")\n", - " axes[1][axis_idx].set_title(str(chr(ord(\"a\") + axis_idx)) + \")\", y=-0.3)\n", + " axes[1][axis_idx].set_title(str(chr(ord(\"a\") + axis_idx)) + \")\", y=LABEL_POS)\n", "\n", " for axis in axes[:, axis_idx]:\n", " axis.axvline(x=0, c=\"lightgrey\", zorder=0)\n", @@ -125,7 +129,7 @@ " axes[0][idx].plot(x_values, y_values, color=c)\n", " axes[1][idx].plot(x_values, x.grad, color=\"black\")\n", " axes[0][idx].plot(x_values, numeric_grad_function, color=\"darkred\")\n", - " axes[1][idx].set_title(str(chr(ord(\"a\") + idx)) + \")\", y=-0.3)\n", + " axes[1][idx].set_title(str(chr(ord(\"a\") + idx)) + \")\", y=LABEL_POS)\n", "\n", " for axis in axes[:, idx]:\n", " axis.axvline(x=0, c=\"lightgrey\", zorder=0)\n", @@ -138,6 +142,8 @@ " forward = (x.clip(-1, 1) * 3).round() / 3\n", " return (forward - backward).detach() + backward\n", "\n", + "FIGSIZE = (15, 3.8)\n", + "LABEL_POS = -0.4\n", "\n", "# plot_functions([quantize])\n", "idx, axes = plot_functions([\n", @@ -149,14 +155,23 @@ "plot_progressive(axes, idx, [\n", " QActivation(ProgressiveSign(use_global_scaling=False, initial_scale=i), 1.0) for i in [0.03, 0.1, 1.0]\n", "])\n", - "plt.savefig(\"quantization_functions.png\", dpi=1200)\n", + "axes[0][idx].arrow(1.0, 1.15, -0.9, 0.0, head_width=0.15, length_includes_head=True, color=\"grey\")\n", + "axes[0][idx].arrow(-1.0, -1.15, 0.9, 0.0, head_width=0.15, length_includes_head=True, color=\"grey\")\n", + "plt.savefig(\"quantization_functions.pdf\", dpi=600, bbox_inches='tight')\n", "plt.show()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### " + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3.8.10 ('venv')", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -170,9 +185,8 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.15" }, - "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "449ab358cbc9abff7c95eafc39955d97c1cf480c5202edcd424b61e46b87f27c" diff --git a/examples/notebooks/quantization_functions.png b/examples/notebooks/quantization_functions.png deleted file mode 100644 index 66dd3ceb8fdbb3f5e3c2ac4c436d4acf106393ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1264988 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�_BkN!1ZDkGrs^T_ri02T-@fLzh)$f&oobl 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%0DtxHa`uT zZ&Rl@&%(!l{(vfx(9I+(2+5`A%*td7j^V&>Cp)akD3&a=r>KvXzxlVsqgrFO+p>(~ z$+wr9xNbk2$~}@ghMpV3%U^hBPBFha8L00|u?RD4L9Sg z#5@}jR31~&@6JXlv81I67H@sT^;k7)2?tXAW*d?y7rn37yZ29_!LeRcYePiXve6F* zU_76>s$icdB$fuk7%koO9-kQVXvAh;aN5g2B^=tj)1dgjv<6#YhceU3O0D{|v(Q@z zJ01>XUPy4eXxYI^WuI!mK8DhRN^&rr!|tTP#-vs_h;(df_9R!aMiuv~;7ySFg6e#z z^1G}EjnPZR<)n_uR`<>I|d-;vP>%|e?XHh-W7T?UJBS|?a zm*oDPqj=nS-mfZ}1&Auw*H4xI%-c(}5mB1+ z!zIW@AV#@KO8NCg%N{=E`2@l-{`>C(Y{<>)t_XmMlz{}!Y#|i9AykwHKf{eW zIf|_)wU{8R2+#J3J9rVZo@xlh01Zz8G1~tF@Sz@}F1m)X$w{`4+polfZs$x(ypp1v zA?S9DK@EDOWjkJbDX`~I6*My(bAn32_%<+6R8jetk3U$-Xey`K*3@!Q=@{}AkS>9> zSQo^;vab>3ktg-U3Y8!x)RE7m1(p$M1_8#}v!OZ(L9OsKc;LG*0|$)@orMm8wvM8L z3S~uR(`JFL;3d*yH*}bmy4@frt&r8Y5=?QM#)4KQhmETce9G8I|0~NQdvm?nWWO`y zaJuds3N;`whvbV!`D;{Q0m&ERI2Zu~jjw%g1UfTX$U(^kUB}-tmSwH^M`-ciS%9Pg zXYIclXGoE@0@M5hxFjmGw`iI~R+N`Td2tYvpYBX7(p6!&ZMPUhv0d)P=!6_tgc1^D zFK!j!&B1$=BLx%c>B0H5jFAdgMh-l>Qa%0*`oX@gN#s;la}M`7kE9Xg>-yjrG3Y|d z%sT(z?D?AIA$qOT-80#)>@FU%`IN#CN0EMrKgEdJYKT8UE)2ScQgcui-injiq_o>G zgpEBD+YTu4^d3j4a^ytZUNsuzTqHXpnxk%AFRA%cnmP>uXwl~=0vP)dftK)#`1&vp zoXxB%cH>ELhhK+Wbjf|{xkXUo0jV;%Bb>Er+*CkZj>HFxZ{5>Z(mP8JKn>uefIWf- zR9j&TwgT7ZI7K?o^+~N4sTTZ;P1-Rsd;SrNZ>2FmV-5S|W3A^Zi1|aPp3*CVwbPyi zwL8@lKp4vF*|TR>5mMe41v;EF2-G2hh z60~Cv_1UO=^^Xnd_!CSr>SBN&M6g=#yd^Y}nH7eXNwLCaO`rUQY%NkfkNJ>;uIu3y zD9HdsZeiq~kkv7;A*>AsLy5JZX1LD<9~Ao!K&`mh-gdnds>RTODxoK-eSQUQc=HKR zcyOwVoMa^L2KdV{DYE7YEJj`)=lt>@f~i2DGFd*lkMA0xAh}}_#6%GI(PVOpd{cP! z2uX)dVlem$nCqwB@bT`8#n9$`d-oP&?)(2DzYWV5T&n{~@0;A6R6@0*u|Ec)zp-nP zXl*I01-NY#LuhP3@#%5PlEX>O>^T&rM%_!aHI990#C07MhNmniWT-sO$`A+BXVnoL zSA@=86r-8A8}>y#Q}$dXAnU1gMYm22;a{LKlFCGo=u?=d>AgjtLWpptSB2CwWaclT ztpZPa9-2qJFoRZWd&X$owLmgI3x!vQI)bj5KSjVbu&$E5)3O((AI6+o?Xk9;7T7!r z)#Ugk_P>qhqw@VU*H6iD8!E3)iW$1ZI^xP}hsq7@PNhR1C@PbQSna(}AZ&-!jmY|N z5wpj${Cp7pRDuBj{v5-%Brv!a`<(R9p>PPK{_vz{%O~4`%=YIjD(EHsBl=Syfod7L zmY`s4J4_US;1_ghNV^Rc2~?cEL>5-dnir zK6v&J7|Nx2dM>s0JlxstL|oQLsWL*YWE`{C;>WUGwAny{g2F8;RwOuz0RbxLJ^FfxEfE%Yn34RATk!f%NQ3Itc#*Tu*E~9539|JR)Q&K zspC_elHwg*2uLDclLS@7Z*q?zqD&&OuWmIOCG3t$X}lLZo%X&u7@vO)@jS_Jdz+U# z8^XKOZcn6$o&&}>LVk}GCe@I1VBwy;w=;>g`~F6QL3t^lwfF$EfFxrH8cYZ>5PQ=4I&=s9 zw1N&y1Up=V1xQ}8=qzd4!{PSoHuHPRkw`s_sy>)<2V8Dk>&#ni<}_KG`j?%|q=1Mo zhFhTZPPXd!3p$Yzn9@RifY(nU7!{U~zyU2sy=+15aSFGy&I-XtCkBIasaY_srj)x= zPfGisf^;hsgYe{!2;SvMHB6KKmO1D_i8eze{tczH&rr)fIKU?u7uoh4bT!CkZFiF3 zpE}ftpBD~~QxGF_5TW(NGjSG$XNJP7^@yMn71SJjdvA8mGVj7!41S(dW4>A{fy5X% z`feraS?|SV@G!j-%=r%@J2Z)s-+11MN+334j+Qh3kFPI}t1<82Z)Tq18N?V%5jFND zq>(};TV<=pRuq+@LW{KT&0{1|A%s?im?SOQQ>3!B(!QKF?fbr+)9?D+r_Oz<`TqK2 z#`C-k=iK+_^Iop^bzSe0S0htp_&qYEHqqB|!6-hZbA+UgX|dG_5fPinBWcBMrsGli zCr&5sfASk!(yn(l@v~>T`z&eTb%&q)rVD#Ky0*;T`!7*aaQ>*CJ=ulgO%fUJ?VhV{ zixzy<&2l@fX26v=HeKAXkcWTWy$mltDoS3;k_P?dPKbHgW_1gQh6nypdE(0fX4hxM zg2=6%G-ye0FsFzM_3~O=D zLGgp#zsBHQ1&h%C>=#&xxPNCFd%znWB@X3f2*6M94KR^>E|#|NRe|pNOLh3 z?wedm(cP%!BSDy;^!LcHwBaxivi>Qv&LOS7%$%kV_@5wy@flP4)UD5*9Lcnh!`!)X zje|<;;cGu(5zUr<55F5Ad(-3K9edd0VA&&gfK3J5%1g zrp&FYe22^0Fmy|-^PN0&dnh}WM6;=8)CB^E4CF<`iQ|Tl9vqK1H#nVmI>jlKEjdsv z>eJ|;2!rT6n3)z@UUq#7T#x1dUtqTHT4z)77iA5goIF%ncZr0-X4q}fns%`{1fRKc zz9~PIx6R(7bSSayiEsEJiojeZ%a>Yc*Sn8~XZokJ`DqWAc86*RxqtCJwf9275ydqW zIc4mO?5Ji#`=rX;xP{s0`DSjP!^qIbK~f)*90FCxUi!K|;M^68@07!)-L1pE4r8GE zbBm>#U!?@V{DYhl4eXa$H;^K9+wgvOel%$5-X5GF#3Qy;LW^^f$%kP>mt-=SteG2c zYsJ>~zg}D9MEVYOR;vY#po$5Q=Wz6%bo-7k+kuAx!hRk!d!1$dt)(+K~lgOJjfwSEVU;>|Y^@Q7xW$+K!O5w@KKBv+6>YlYPbNQ_f0}5B zW%xU!INg`IpbRPvc5jTYCo&$VJNmDk z)T{^n7dSN9;Ghaj8?fnA0?StmYmg!C4|!&h_fP-Zw^Pw_{ByL*VEX($u$jo^l=#^n zCT9;a>;VG*oru(v>RKI!qWhUiOto;MxaKKPL7+#oQ@f4Etv6 z9&zFoZF)Z^5Y_V;ft8EwOc$}fi*Q{{Y{>zE+b1XCC^2f4kaHJYj8PY9NMBv7e2404 zDpgvgG9KKFXj3FDM_r3wmKQwSoLm=im6>93ld$aNT*D11zjSQbFtly6@#s15{nEi9 z43)cE<7xYn>@S=ub82Y9AND=@(Q?3q8rNh6V;Dlz{foLg+~w6?KcoS?SGO<>T_6R> z;fiUL-?q)g@~#tlauZ6eO96Pfz(X`CgB%D{R<&Djvp+F(o*pT%!Xr@y;dN9TY z!`k4VvCHi`9}Bn9a@(XxSbesD@(VDEHjnfg5&s3Q3R7oNg-PLpDHs9nJvXp>R_co* ziq`=8VfF@SoY=9@ZQw&66q!4G2PO2K33N;>T%P!LTDX>LyKfl`|EvYTj;o+Z*nuj$ zp8V#4f|Ez~UI3$H!}w&;MT?blvtLQB)>EyPER(J7;_(%p?t;v6J zVotD1Xl5d|H51{_44BvKkm`6p=SmOYY2pd>N-#3uLe_RkUl5KT3-oSbc3TcW$ibB> zTqYd_Y{022CqM^W9V&_l5^$L{# z>e3dotmjUh%Gy*G6c~#J9au_7i%aN2lSlnOfbLA)vmVT`3s$K^(pVezwIdzY9b9+A zKbB4!=cKV*ceobbR{0_-YmG~fsiZzzY(bCP{O674&e|vSSUUHZY_HxsmA{k}-hdb?aBLxxYj3QO-2YXno`^+?n zwCk`Qh&o-8GSg)Z1~h{GW22Uvn@IkYlIn#1LRL$LOYAn>Y zB7d3Wh{a6}C56@Jrw6a0W<5q!pPV1ZTC$Cn?S}F=qNd!YS{FtHdwyWd>Oc8(xFjj{ zvpTbpj+V+mtER+L0}nJ^Y3@RPHdFbBsbc$$trd5%z1an&2%DjG8rZyOb!$BL#i;&! z989z0uj`gE`$qLlS=~*2Niu?dn^v80sQLRB@;b=amQERw*Qr1S$WCuD0L*CH+D*ni zsED2J3LnX`QYQjBZ{sZKiO=E|o<%r|SD@|`X9E22S^pqc(%gbIA+L;aWG#fe4tj; zuD6+gyUsznBi2_%?$~kRc+#b_!&+%KEW34O4oH8ua)pzT+hBR0*1JRUT#-V5X{*cA zThjK-9wGrl-q^pwd|Tz;6Y_>1jZRns0w=m&E!df$2}@~G=L==QX@oO+EbJ~_kl}*f z4tBZUM{P=G*hiOMI`HCDeAKIQtnIBj7HWO$w6xx4M$zf3FdSG_*8UX7nm;N}nV$OW z*fiN1f=UELS?<4gx&yN*rV`tACH#+G*S7Btg^Mhol&DN~X%s4o9iNYuofL5r)d;|qSVs*|j~#$ch6 zAdQ_Hv%@{`^${(E#$hiLQcm1fg*EEVLBxmbefo+e6{HYg64&%bIQxr(W9q+ zhsR12T>s)KodG#*!C$+-1hR5V_mR#%yeL_;u8Cu7$h;1FDHd0_`i`M+ zyH*|sUxaH{iaqV#Bv@jsvaCiTmbY5VP-129e~y{nQ&Om8@7GS2`clp@L$xeP)aeo1rv*2S9bs#sB0 zYF2QHP144yL}7s16%HF>m$NQbUcrE)+$3vD?Nph)bpQGA)a16DSkD0YB((m>C~F4? z3cvi0g<|@<>mFTAyJO@~GNdv3Y90T>E*Sp{3n^j>mxcW_f?z;?JK|gLnbGA-xL4O3 zU!CSk9a}GHh`BaC5`MY*aRl>%`kWLw_YLER99{H~HJ zGKi}b-2g}afq;3R9WW&fPe#6ejV5kNsDNS2lJIpP90JF~(py~wiFryRm9szy%-kP$ znot27`bxQuMC6YXyU%CNpG!Zp5lq#R(ph$^1&8uNZ}&tO+7E4M{UpvF4cJNOZ&$I z`wwYg|7H?-vUwokp;is&9eUGr3Sv;k1G?jCq+UX%p85^zL_wwP17}8!Op+^Jllr>- z?bPW6x?F)P{zyU3-0njD`@TMg4B#p?HE(GLdpxrhWtBSNd1p)px53g;*P-$}fbP;=ZtdoX3>WuEZ0u*Qb!3iB)09`=1j}&GEOleqB|c=p44K-~@kkX{fcOBqIh;ipiF9v+&aO%MaEsIlBAMufH~E{`kkLB|Cn<61mCbW!tQwyLX@P|6b6ueN&zN z$W*~BuiCUPvqG&jpt&qEE2!VzSGrndEnPLW%S11?>e3Ml-2*eqB|fSl%Ai8S-P=S- zYOp?#D(M%&`~@?mQ^Q*C{yxrpKZjV^K^b{!>f$?eOpcrr%0C|ddFJdsCNPTh3mR#^ zS})D&*Xt!|=>v!w)$#E(CB0v$6=^!@Ox_%&J#8K7>462OK+SMj7}507xLL$ufm%yE z0K>6Yl>F=W4G($G`R6!K)y>*~nr#xPGj5@)9NRzR^6+pkF{NWxOa%lcUMBVw^*^x= z|@*2r??WgK_sMf*;YQ2c1UsiJR4mMd!-KJ$8hVo}0DeyPe+Sl(UqHV|| zT3QHqgxi?TziiU!Ta}@+Y0N@nhs_BuEIgXhiU)`_G;lf(5o!b3P6}adRWbfj_?KCD(aKN?4!}Oce=``Z@SzKeZn!VWj2$ zSnfCC0o0sK-w}8^xqT!LT|G+;Z)_|e_v<7{P`bS|o^VQ~#Cp*`l7h5btZcQl;8>SweXCG!Zd?- zfA-b@OIrcQ<6Pc6R|z)@xG!?mt?`rUR>g-II<34ys9QC6$F*XNHvc5ZN~~wFe0mL{R}4zS-vE3h~55g0g&PaHTqMac!;V!MQDrl=;Go+AhG_2+vi;0va1|8;0MgF*K)?Y5*C zZr2eLDbRM8X0zwXM2gqHr}Sb9Z`k>~f8JRyle^)3dFHlfuLNh9H7SR>`L!fk?D-)n z{mp&Y-vqHLXJHa|i<4t`A}3XDEsu-zYsVUYy;&9@a~OYa2wK&oBJXyi0W=absi;r_n9kh-pgj_Rd?DHU9*IS!!D6 z6F8L66%fcZ>2u_(&CpXlL|B$YT{lY9c}=cKj-J@GCf5aw6|=a07Qi#JYpV5*F=(7m@3rw*>WYCLvRRLIlF$;&1zXVY?3GC4S#8|g|4Hbve5E(u_o1x;Den=!tV{!^;_%( zfRwJF6U2<X4Y@P3w9?jM9b>1dbQQ#pr zI4OW)_te+Ho(95_M=m}Z*I51OuM_dWCM6y1sO6J~AdGgvNJVLw)d$*A7XYzCP}ri~ z;d&S35%}Ak&Cn@rhliYZbmIC!l~6_LIxFx=7qLQ&WV2ud29*6s)>sDNQ9VwL1!W_a zlsd2mfD}vD+~kkVQyxD?;UA=n3KiW3W?wq_!^8jj3$jmUw1dmypo9B3LX`@U3*ug} z5(x`3K2yTF@;^d}A&X;vM?0*X;jE`Z87&i;ClX0yH*2W63c7DteqTD1P*-k4CsQ7l z%D)^6`Q=j0GkKDQ&6q(li)Up#@OU9}9_Hr7%|(Y6Y?tET)jX_l>iL}#Dr$vz~FZ;v%Y`>QJNo~0oYJ}og*RW{w+HztVg`mh)ZO_=%iW4E&*s}g{ zM9IXaLvI!c?N^3*RVe>V^*1FWT{6|ZZT1o;l1vDxP*?Frp?(Bcq_RS#EqnqRGBRBq z3z<%8Mv1R)j=UV+_vG+3?6{9aWp%8;w$mfWg4qKU2t%y$evq%Y9>Kdw!vhaa&^)fu zr2O(mk+gK!U`IO6B=he%Rpd2udTvM0ED{{9GMMdGMf>1cfWZ?Uz=HS^Ls7Hd=0g3x zGMeMcGi8kh?9CDjgt0oa zDOf!e6lgL^`RVa%^OoY6-r@_uKJIT^B3^XH1KIOisf3A6>7~I20*B#Tfv8EJ>SPy_ zf6E=C(^k#)&kr+VYHB>HI~dFjKx$`g`Q2@>!<{*EZ1A!114SxS_=V@O$7U~M5?ECd zMwpZ#c$btU&QV)`PpEc8nFUwL&Huhs_G_cy!%aaqGoy$wG`c))eCOZtC`Ye`ZE9v| zS3Sf?9&e1ccL*??ecZNhqhE!}Q&RV!;%*9tkNiy}QDHm@vlL#5_x~IVp4Ma# znImVMy>sfk{L7E(pI;TczIBt%^0GgR^a3s0m_X0Rt`|>2>}PC(0N3rVbDj~V$(Teo+8i5bvft-zuOtvv_hAw`Z+rfEx#Dlf?kxrlx| zRjud!9OKH!b2$EjvQ9vR1!}A#r+6JN0llR8vFESKEemzI7PO< z9bHp31r;%b_!5~dVNfZc$nLGUtljCr3QFRPllUJ~*kIJ(J!J2W5j9ES$&If?%@!NX z^uS)8_iR>*Q@|$kQqd{U6311>fEaMdUS;eZINnp4NJ;UT+hIJan!$Epdl=mgJP%q~ z)_%{Axh|^QD2eV-^bANfbzF(?a)1X}4!)g=s@iwG`pDq@Z5NdF%f6L!&FFfb@6?$DKL;r zbB7nTuGvbYBr7JBDMn*oz?(60&J9RU9}m) zHD3R9I2@ceN=Dh8VFrtX;f}^&h|wGFYiqrO^oULpuUM1n1?P0e0k)uWil14Q)SL(# zYppzLm?n9=B_z=#grcWJ=Li={oN%7P*g)GiyAa2T94JPTP&WU2X%ZA^|2f zA87u&U1!(*CiUxrH49)){?h} zpJe|!kUFQpJgcez1mKe+&y_&IkisOyO&d#V%%vKiaaGexzL1At#2P-{_3cQkg`7e3 zDg6#C3mbnanDmU<(pX8R`_FDc2V1e}{Ud{^i#lJ2ruJk0!09bCQ&v|c);l{NetvZC z)wdypI8lgho?8C$LoVT?Of0Rd8h)N$^{(1oDee*CTml>lxh{wL_RxQ8zuS!=vt)%1 zjC3n?B>&8}yve;($Lqd%<&{HadV%x-ExbsDC_ItQuYmRlW)Q=)VUSU?co_hDls)=U zFzu$<2fMuyC63MN?VX>yfbq4Ov6}r@+5e2y<3M%lg~ew-SN#vYMTF@&Bnx61Ug6tplO; zj6mGXZL3#;LV>=K`!AzS%ELa)y_QpmyHa`}HdkKjZkz~PkI_$zu=*@Au zF61U8^wWg`SpJm)%CH`4;XVxyCY274oo9KEjudCibg{?0y;-_#U)I(asBhOtzU7CC%B8t9@?J~@?(;*n1KY($hlyRL837Mch@579}*{t{+ z*S#=Ev&o4JppwQ7G^EdF2q;hJu)Z=Fel~%JpVyRE#I_FSxL4&njP#vhHh#EeOkN~n z08Jg#OzFbq2q3`$eoxLJQN?TfX8xH#);zr~P)Fze?(bhjVU32WdhW@Q*sRnPDA`R@ zVz6NC@|*T6bP3?7k{&!kj8Cl$nB@&90RFzD!OB{v?yaA)Azi2~W#!me*nNj&&6BMK56LBf2={_brPuY(N@-?IvYLgP2o+P7bB zm~v;o@fQ*M*=h-_9-8lQ(}lq1mLj8Rg80c31lnfmYAo>W($4$m*p(jfV!gmE@aLb_ z^!Cq=Y|yeY;7X5uc5qcKw=OzML-;(Q>i+`0OjR5uTd1l4BzXebwv>5O2$9icZSNo@ z)c);n#!m0Xnn=qgcuT+gKH{WbQsuZ@M$iJCjVG&byudjvVrmY#7*Lj1<12`ke^JBo0b#Xs@r z=}HWD$)q#ud^}$yVrLw>Op!R12k}_6%_7X(VT2M9Kb-r+vL?uN1_&c>j7PmB5@2iHC^hIx@aCdB;f@eRtHHvcOCyQ5+ zW^GoW^O7$`{mkx==~K2c;ma6o)QDI)U)|nnNl1sZ^mzXsJq?aOiL1EHt06JT~Qo ze_E!)XEm+VW$w`ErYSjWOB3QY$4z$ner&>&Gi|COUP#s_YiS)YCYTrUmzUr_v|HP2 zn=VN+<5nxO;pU%`KNnCX6I4>ncm|M2Fs20;OK;Lmn>mhx)V>Z~%~D|80X?9-ocxbdWINPNB1)ug`UqkOv(C@Y`jd%%IM$?;uJC+c zi_K>=!9Cf8eCkdLXh`epp1{*gLbVpYB0h}3@jS!1$rJppnbUcXQaTT1e%`$@B7 zC>z7_SVQT?SlUY789m;uumaY!7K1sGC(pUIfkXFy9)L*w%6Ipr?W|Gxdg`ZmCBX%$ zC#IU~4BqPt*k|zZ_6k&pH*JE5h5nR3y$;hWw^YyYl0?{jU?Ap{j_RSR~=IM6b_QD(hycKU871#V3PxTfoAmY=%AWgQX|2l2tWxPe*{3G1F8sjk-;Ub;;&_KgtF4-0ne+q3hiyxT#p$MG|Qx^h@eREgGE z4UJYGq=@;if0=`pkn3j$9UQ&L^y~$hp7mE_dS;9;NiH>=D66PkUG7r*q2s3@;39{Oo$+y-pN!Gn9e#c6gLoxUa70`$E!0Jp4Jvr-@6)6s5U737`*l zoGkKl`++C&@(@Jz1F+%`x0lz|J?OJpSiH!7i0-TQrVv=x=`K7vx6WCW7T{$B-s;xg zp~Cb4luz}Gw*EmE(T;rJ!zg#K0XTT3A)byOUFjLIgCt8?=kh1Op#AV6UDcYf2{0%O z^|y3FXat24J*H2k2F!f{R%?j-GtC)p-S8ejyqQ~#9^zV%uk}B#)Mwm9o@TFh}aAh4?CIeM_m0BYbmnYS~e_iqNFrFKXMvMSMy6u-R(yT8QT(X z{wwO4+paG5oYG&x){J=Fw($(85#onMnHenJpr#L$bHzTu%XvSSm4(?>z5~|OKa}Kt z>Zy_vP0IYBF{x(E)fYE0>VNegp5*MPOV}h<_H_<3SOq+&WX6bHLhY$GIw=0W9=G9U zz6WIWbqc(u+s|MXCkQy7`8&yx@az zwGz+LrJRvn;HlCchy+5hruYo=lk;rRMELrb>v(aUydWxutBZ*|&Cz>*d&PlEqbwNF z+AKezVC;H&5uGC^kh*i~;-klBO^FadaOsD-VMW=iveQ>n--FRdLyNX~qw<}qP$ zTpa8^-ccV2c3NAj+vTLryL1Odri4^8X?fbkD0YR8;)CCU{cIsB*L5wmZn|%uBOBH; zPVVxm9076~8;m!Gc0|Oi9t1+!x7rE8A>ODVP%s~aaG5?E?K$lEk>|+5r!w7{0Cdd+o zoWJh+cHgkzBG34vZnKoD2!q9{+qXsdZp;emt2C{5;8jnsyP&xJmvAcyUG*E~T=>vqN;zmc5YRjvaz={Z;q3 zp50G!C5}ML#rLy~t3vJPdFjLNkuDlV*oNt{U5E2SPA+i&;-#|p!latn*}0x_Vj)HI zjI(4yb^;T!^B-@+E!JkJMEhOccAAj4;{pYKCh%prP=-2kt_$ZO5PVAfspbL_sPRw> zyl7}7`mGM~g?(hx%Uko>CBM6@y<2q$1E=w+VOrh(aW%ZAbH`tEcpM+qfwxtq0w>&X z`&d=i=X@uB4NQ0C0ypx^OEt9yrT>UZ*qOK_JVkIE`<7mN!3`d8CC2sa=+8VZ~P29zamlZ zFhvoBxf>Aq0WS$7^(8V9tjDM$dJDDPfvy~9o&hb;A@85Z0&!69rnSbwRCDZV?C3c8 zoV#@488^LNoq1cb)%gI~+v@ZYi=p+0Y9+YJw-mos7^d;wCP0$;)8!L`+qlqA~DgOeX_dseu}gSPO~_ z_q)b)z702q+LDr}J!{P2|0QYmc4J-NU|^+2{9*@NuCKoE`?qmlou_yoRWh}Mv@+tZW3d7PC_i!7$Uu)nU!GXQ~H^+h%YKz{HMBJE$&l;$R zPC%&ibhu_!SRFX^T%E#{(VLQRUsj@QZm^%_(!R{J*Q)xj+un@lr&nartT(5JvT#%K zOzcbCCvk895^=AeX3}yC$9yV}BQx;9Mr!)#C}um^s`8FygJxAGVx_`Ek`e>uv0v=v zNV>zpxYL0lQP<}Hv!YYt@9QVv?K4CHGxuOjss02rh3iR!!2~J_=k-dg-E92jgsyCC z;4*qPs|xQwY7~?Gk~pl5w6;~f?37`NQYiuGaLLBXESr58LE@P z_VutL9a5k;8HDc(=R}XlF>0*;VjKn)yY4{a5{F@VM1h52sq?8QC_Z)3P43h19jDj& zS#^S^;(%9Y+Q@n_&Mh=!NC@1&HsULklH>lL!#?+1fmLccN7NzsUtfQW6 z0Z&aGVL-Ev`0XbG)SOM&wupf%S7t0mW=wYS-0UUV2mg46kFgwi2#M8h9r>R{+(qM? zZ*xC{x(yGH5&{BoSAY?=fWh1nA$!Z#aha$;iGCWZKcH3UzH>zDqd_##r0)>3-T|xt z!$_`ov7t@Y%lMbswSWIIN9!953B*rt9BA;hk*j{7J4$Jd zzyc+@YS!bObR^)_amCcO;y-XPw;s-vtgeD2EWn?!SmVQN*uRFAy}!owTHY)*3}mZ) z^yX#!^;gXBwrU4+BgX-%V>~UqUpuq6#HRL9?Sroo2BYl#IP<(2P0E z4X6JJ02H^>to?F^3F_10uQoP;7d#@mI2UX=-8n_=?%9y!$Cq zV_|pZ{n<+3D&+DG+14xztJ}AV?tFM6-6nx50Z>xx*+cxSAdl5Qp0)18Zzu!{JT;suuW5e3)4G)=2;|7>HO6&Sy=|n`7pV*qG*fTVvuZFU)JyzhpT!&=n<7vra`j#+L{?+Ka+xfwAY83Ce+$iD5T7oXgTQ;}iu*FcPC_X+KuM$&Rxw!b5)n*6`B9JbFK$&U^=+IvEH z?C-&=2{$=CG`~Iy zkk6=2Et~Vzz%3;B=6aZ^9qR1kux%oLZIkV)qp2&&P7xf_5JBk5*aT?J_6T&d3x$=J zv+1f{NxiSRzkBdRm@%FTaZ(FsVe^TUF+`gk*H+1$IT6n5@uthCko78s4h1lV`q5Aw z-+trV5VhDpHf-rFwCpa1;Q1OD6tRosu?sMv72Xmwu;_mg5+J<;ZSf-aWQH%c`Y5Dh zt#fbTvhkANg!>%kZ_&7q)0zC%OWIV zv`=UPC)AE2k2x7~iU*M$81@G_?9Xol2z4$(Cv|0sksl2zUZx6HL`OgBIYbqo52iK| z;W+?cQ&}=%*Y5#iA+{|W|2o3~>_T~Ck_njZ=_PruCr$!f`R+f%!l(xJ-~%2d;~{9 z545%fl#i{g<4H{IT6AW3b=5+4;s{`LK7YOH9h)GE@ht9* z8ZoBvpWtZ*tr1z_Cu=(_is zRO#h#Q<9rJnvNo;u=tkw>Aoh4cZ7X83%x4tQhbpTJszj)uVH>S0Pl1c*eHBEvUF*H ziH8CRQzC8#jA7vLV~=%fEscfNH-k@x^Jl@wN3)Tj0TxstGbSopr-_TZg7oQr3j70 z$&j=mKCJ8FfL{Dklj>*}n$y+(iP;O#Skj?8@C>sOoXPI3CR+VZy}0=36?M24`B}wS zk_~(Iu=-jrDn86s)1PgWINVuy-p`J!rP`Z$N=(+e8y-tQFiRy%lpfAq?Bg?`uY5ug zJ&7r9JaZjgAH90kI1$C(HecFNCg-+)<_B(3e(8WID1&rYm0VFdVIKKE!t-QXk2$K;&hk`WSTNkeSlQ&c zZCk)lsfqR%Zb#ST>}$GA?>e5DqxCHuwpvs!?Cwd?bsJ$v3iGl+IwfUKo$QL>(E z?r^UnL+^TIJGG+>l$8lL<1o8&ga?X12<$myhOxINHE96Vf>%aDmPI&3B5XSy0`IsCjGENlU&4Q4$iatDz1p-IRXzp08gPuz;Lhh zils}_n!^tyn6;MjC#KBgB5p*^u*jKv?(JF6dASQLz8xArrLJI~2I9CSCmcTjlMNdz zD>%a>2kdLzmut9BT*aFn1`)Cx39iY;z3X7SY#7(@l&vj%V0M~Qe=44OqcWG zuqx$$NEUB2m}`6PI6G5`J_Iu2WpIh&J`Ghk4dKm|SG60Y`)bOGgdnWJO|o_z)zLAD z&KatCOhBATgUz}gJ(AlbwTt15Zhdtz*OREHwmbF(Tyw@!VCPg9zPeHF25)sE_Glpk zzq8g(cXNfHDjKd+aSZ!9N#m>s*gfx;d((>b8fHV%+J@1-g3*c21?+!sKk~lLB~fc3yGd9<_@V zVtq^|EPLSk`kO_mvHYV9EHspb`UJt^m>pm*6}$eFeG)T-B*hi6f|IMedONY8;t^1c zg`vR@JdPt8=q=h{WGsU~j~y@OMJtGYa@#+7T*ag{HI)!w==k<&*!g2bL@Xbg2qp*4 z21xAHB!A#Ub45(OeXRj&K2as*Cc{s6CIkvkn*FMpvB`xm9U8}a4dN8}6ASRx>mYA> zqi}L|b)w#N3V>s4$<%#>LleSp`t5WGZ;2{N6SQeQ1eeC5Mp%S5OC%Ba>B(;nJ?(f9 zoMd*{OPEujlK<23f82AKrn)FBC$zUnTL8lVkb2KSgpBvf_4OZezW(DdYco)$uF0s| z!5V({>@a5K)pTP7+VMi<>~KhZW=~n@YT5ru7$isZ&Ls*HNRB-D@|@->@-}Pp1uekH zM8=BBx}i|Jy24?=lj|g6#}37Fb9TnUgi-;b!^Uo6vA&t@jq8rT`klBQLwCALskiID zOSQ;EBIc_iq%6zJI@}n;E4N9Mb9oABUK9=z+X01vYoc@%lDU-vT92dturukc~NvQs`3?Bm;4qrw`lljhC#-UhT5dZ;P0kTY_IvV zvV6R#r+~5Mxd>f(v-i&2k-=9YaLqgFK15XCSu-J7h@d`$iQJ_&;vXguLeb1i?P?!j zKqk(Rq6{p=ibK%6v%wuBX!NwyKV)Ec+L)4z}`slT6e9TpwyB^Kr>iN4VCqrb75F*hh z1Qep~AcdutCu(wE>^uE5&QEB@H1t~koxUOd#m&qZ?8Rx|J_X4d6gCa2c4P*_NZPXU z3Q=YGkTr_hjX(3fe=6B}pYAVqQw3%5_i|^xXjtU$VueuKPI=U0ktRm*>Ao@vafWRd z?ANXv!mXjY6?Dx8r}e$%l23>0<+(z{ZuzYo(j4`X9L*2CpStSPSqJecJU2fPp(i#% z5Ge@q#7xO`Ge_J<+H*M4&`W5u9L)=oKpwCh?g(oS|9qJAn`KH{mc+prar}^qUknI+ zv@TGQbi>5WeVL@#n%r-`V4HSQ1*P!uOX-U#qWPg@MoQxSkH~XjEifXb(U)$JW0rEw zZB=TiqT0_s?iCz)MvH2!tdA~VsA6C76Ve2{j>@e*ujhUUF8{wcDbh^j;DWJ@|3JNi z7TLEe(*@ja`dAiUM*UQLA|gp5fa3ma;Hh z;$wW|X@4}9GjM)(e5RQk-+Uo5hdIg~0w-hd6=r`0nA*@6GV&^QBLv=1jxLziUWbD( zrk?5#u!Q_QT(-dG0RcJ;+ki>K6=7Wz-ko*W;;i2BZ!96tf)=Y?x$xcT$ z3`L0YIL~kG zhF4_d&((Ej8}liY&sR4;fu|t97&tQL8Q>ROS>o|GoUd*uT3=WaOL}t@>{?X?NxqzJ zf5#9hB#Amgf(<=|mJ$+lPh;Md9y$1x8(Dw#n%1;4c(4-I-}wwb)0BN(b?MG0wbO5x#hmR$@A0q`t5bzh5GK&bou6=$ocLoL9HA5YZe=bMslD^ zPI-GM89FyXXTtuenAf2rW(9)M@rwg6Vcx%ZaahW1`I_{!CTwVL++UY=fF3Oz^_4*R{%F2O5 z9Nh~X0(yM)H^HHAwo&-B0dzEBSQs!r|FW`a5uBF~kd-n{(hMvds=F-21x4}4eR@9b zX!1l-}dq%E1x(dZ~q?d+WZ$w84*-LlB z+1auh1m3H@)Ydt!bU9_SMNZBC)mv%Wu_w67!LbsD3GpR{SnH4tiF>lXJRtOXx>U)w z2Ro*m3y%DZ4P)-9i!DkI>6u`43bX=;;K2B?4WG21>Fxlx*4j{;Q;V1pesnF)GsEEJ zHxBkdj=?nI(lN^aUW%CuRVxIwCdm{z8O++c+^xUWf;n?+dC87S6bjjjhJ&9}s%qh7 zLcaHw_Dz1*B+wGtD{}Zs`aD#>W)s@`8u?>d1L~;>mewyqIauuK;%bZX1$;iHhvCxt zA1hIL_$Y@lA6=!L_OgA+S0_%Cu0%HqP9M-45e623FBNXzZUQ}}E4Gj&qC|L@ZkQzf za!b#*#{z_pyozD2#)UFut4x~A%!2W&OPmy=mzSR zvNX$*RO9fALQ^|*%l&;5xA7y-2TZR^)J82@?xJ-Xn7{7$uf)FiQN~A)Rb(~}&qb}W zPj@?9$mIXgx18By9!92|qrR~__FpG6x2$*^hs!#cZRrLh9YP-m(E_9rq$!YlXNzNR zEvXFJ-d4K<7`Pfn2eQOI4W40`iUEZ%t|wMXW2JR=ImNealZ_75^|e}gZ@t>RQ6N&? z7cP6m>|PwJ`1)Xx*g`yuw`!Nh?0S>eeps1u}?c=d%4R> zCGh`8xh$H@G86>FrRAM9sCjG-hiXO^3V*vF z2CN$2P7Nf2oFWE1{@=sFHBE@ift6=&D(d6sr4Da@rrcOi!TrcwF=RJfE!xqf;XdJe z!!4u}+MRU?!=1ZStKFo+Cro1&QN-SV^3J!qE-fXld`Tl6?pVL>j`i!nl`6dqAJP>v z&;u!IGKsHA$t5L|2Vo88PF#A5OUV*O;B6wf0#(tbBO8Ajj8{8&Qbw>;A*|h zdim)!wF`CY(e z>AbnAagoX)S~ktb!BMY$tt00*UH`E2;?BK$k1Oo`edihJ7e7ur{mYl-fAReCYk29V zzMi2#86_L3|Ac?(8`RP9%}N@~uCY&SS+;SY+C+noeLk+umA#cvIpxKO6{|1Foez9d zE8PMZp1h53xtB0@k*MFQ_D_!9aw7<`Yh1qxLX#iUkkA$06C4sE%2Y|ZetM|c!+`Vq z_gJcnoij5xUukp$%X`H*l^9(%+?UuMLure!!u4l>CfG-!1#uFhR5ym8h72Yw3}yY&bBuF>58t+&H;sIUF74`o^@z9o}Yh z&_{JYC-{-SGwNvI-@0}MiT>Q`r$XDNf>3urd7i~mRnE(_KHOaLMEtZ(`xiJg_h6%l+3pfI$+V)raxXK^ zH_eur7;^V55RpK*{s_N3W`;tY51-OW2fHYi8#?{3B1uxJQ)hhOc<%79o~@onzV}ui zbUP7B+qcH$hh|Bym^o0Gu9cg_3ii7~JxcPAYtR7H@J4Ggv#M7}Ctb>Ve&oRTmh>Kq zt&akb(OE6Kt3t-VU!SI?d8&Vks=S%h@3N(4nb1cLW6g2oQ&+&gv!w8Zx@dVDnvoh_ z&8{-ru8c6lvbbF9%Ts5Ol>vA1FSpNV{Rs=sCu1Jj z+{gJJBhB_`9fBV8rIDSmS`yX_5b#9!5;f>fJpZOOAZPt}ImAOl9SPAx25r;U{9&|lm zZ+Uw{J?nWyM8q9M8ylN7iz|AlwT|#eJM5Ld$mHM*{*C_MR_f2PkE!#`(H9Fh+*1A2 zORN3EtrIkO$Ml@gl-{oTjw_giPZmLH#Wm@{|d!7-MRb&X%e*@%OM_zcMw$A zeS8QFpavM3FB~eb;Or|nuX(L#j*Q1?FIlPuVIZw|h}h zHJp1yo>L(b?=mY`7p*Yrjtp2`<&-r1*>doySGWOxRNEWT7A(*^e}VfA&`$u}rB&dQ zCo4cDzMJrqzAwB@MA4z`Z{ii&E;UoSJ3H6e_&&V8qj5!Dpky%}q{B0$6sb$ps|=zH&muhm61Z5fDy=oG5|hEj9wvC!L`1hCqxM9(_+;VQoyb(n=% z=@w#zTqeYL6H;I>Ck6U;mO?+6V38zw{kJz?qtiI@vxM z?9Vw#7Li#6B_<*zSlYSEuq=t+@DuT%gAc0erZYd8%#xm4;BBUy&-?n(?rJ6s%<0#k zEOg9d*4RU#{#`GE%C=MPb>jtX9-b1d4#u#8lzqnLqQTwit}Y7zzcxAkP{dgZkbJ=y z2f!%5Oz(XHsP4Y%a4fu2SoiISHrcJ0nk7B~T`R zNk4>+fxfR=2kNwYBhQ6(r1C`=P+|3|ZL>C45VR&AlX?Vj8Gd4lW3Xni=GW^1&I4U_ zp2elu=}K&mo`^3Aa>Yt8S=eNn?S7)_YJ$=Cy?ggwaPUPQ)`tlNs8H2+6rXBsEXa)| zlR8}WnZ*@vMK54XDRurReGJ>Jsc(d@KTS4bL4iZXPaTDMDs{fU+4x#&dD}KTIb>jv zL1R(=j7zxr)>vn9prP6L-7il}#~uy&x8?=hfErhfp%TJE;sjjpTMRaY4d%#4fRT6Z z@WH{}XqXk8p+j(JI?~}ID)=b}r=kNz6;#lRg`MP59B4YW_U?Ttt4rsk3k8QSx=RPd z8f)kC28pQ7y)5tcg)ToUk|u&Mw*kT=9D08AHnPNEK#=FUYXcy<<@2s$bq9qtc`>}{ zqm$qpu=+MJvdJb-T^5!(QvF~35csNL(b<4~Vv&)rzthyf>`*J@!6)%K&Z|H;C_lGg z=6n7tT9 zlbBuSJG%!|-P*eCVX!x4bx^5^zBWmVlix zwT|-FrpY`~Vq4`f4`9I-vCXv#QV(5a#1q<* zDcN=dV}8&4;JovqkXhwg-z?$9iUw1V9rAgHyshEj*`TCb#`)6i=l{PE@l5prkyL~_ z?XrK46^Sou*L9e0#4Jh5pkvbd$_)!0%WGQ^at#Nn_)M~#4?#Et0?*u{>c8jgA72zk0h<0O5E2sdQ9!Nf8X(pjq^l5(=gQSJkgz(nB{|SH z@YM<_k%zv$%@J90a1QPyg%DD~xqm5&mv~;a?{JBM{dQ*D={jgn7vOy6b1FI>ufrLf zNJTvn|D@H|ms$m?_Pho%osrkv>J|(|j55r-qId%ty-`~#B&KQ? zoSakOYiGXa7nykV&zVM+yS+gHZr-T1C!a2Hh8d-DU+1Wwc)eD++)Sl2^JZ@^zkS7D zA0;`b5Wjd8!l?xc4>xI8F40l)6BNJ}&!|cTp}{{F^{P1B4dLW0wUn-ahI2>(9|bN? z;k^z*$BH(dP(w5d#!Vs7_llw6)egXM`nIy>m2p{IO#RK>OZWFn7C$}uPhNioHU%9x zaDYBhJfZ0mlEFSI8$LO_4{PS?rwhi^2gFo2X=LRaL7;hmtA*i=a)W5D3wZ%H(bjLl1GA3Xz@np*fEZ{%)gskWYT!l0lx0^ zPyIalLHt(cD)+^vAinB~%B3PNz4Cr5@(n_r6&M1~5)&^{XAD(J)F;@*7 z^4%(@`>|Z$Y+;p`2g{+V`Y4bbtQLFeY^h0>^b>J+NqAkKJGbWbFI$J3>jSl0uFpFm zv8V?&U@`kVxW0Dbq6s+6e&8@35*P$@PT|>M6jl3im3HQr2RVtx+EKjn_TPoW+SUY} zp?mWv5-o_#T&)SK0Ag$#za1ZWH~oagjD@006*XS{ZVne#u9Blr48`!Ob@7!>HLm+# zFN|06^&X!GrQvkB+$b_CYNe6>OB=Fn1fr~u`9>wi=~Nb2xLvA*ldlTr%N|;8WT|dC z5~h`U{V?kwfT+IBqS$1g3|s6S$ooNuQ(ymEUhh<&;pCNCeEMY zm~m`!46K%&%YOU%pGZ@^6>Fd3bp0}-PLFKLbdDY=V{qXUQD_B{d(Sf)^Ng{gZh?q& z&(ZwFKbes@pt zT=aSVh8qIgSsnUJJh4@5TAYb?Sp#HGu9k= zx(6uT>tD_eF~57J`(V9zKCdMBZeJd5TE+GK*H?~LlKL;l(7-C?Ge})BMQzEK!qk{0P9_WIeW{oPVl2ZXx$Zg!D|E!khQRg;+->+KH{8MK9H$z zH}*Hz|3ZDtu7DwTJb)V4N4u}4Wk1^OVc|HUc4%&x=G*@r_L>T7u^WpeqrR=Y({km> z0qydJAll3wmJCGC0+8pN%bJ9@y4!!X_v!DTSvY3Rx|^}CEB&#TMr3{t)>rR z{V%ZKC@h}~+drG$qKG!_?ppg*9*#cDh-z0TYvKSeqHxDimRV+ zKq~Ux*jIqU1Pj9Ldm8Uc8>kYbL0@1dD|xEcaL{(HEFhhVEU=FtV*(U1*6$$Qp8Qjo z?LcR)HQvmXI-i@5S*3(O)*6>@WL)8Jxq{d#CvrM4PE7(lU~yP5X8#qYaf7 zw0F{?PNn6vt4`;4J?GRp?=s&{e@tHUqR#VvKhOPK_jOStYHq0Ipku$}f|J}+M42=u>P=8>IG7)^bTEi0d`Uq2ZO6dwhb z3n!il&G;JiCx?!c8Laqp%Ef!+M^#BPu zR#~Li^!JPwA@2MRbpWLhOry8auXuh&_Cj$`lf*}59`JSK3{wf=STcOHhTR`+Q2V?Q z%7)dNr|wc>UgB3VkCXxUDR80{N$mk^w*gL6%DXO7-p>4j9{!yCmWhS2^pXDEseY5z z<-P@zXW~j;r8a&<&1WUt9UV;tTfXlIxulb2+x>2zmkZE}2N87haIoGGlrobf2r35+ zy}M_z2@$~7jnqlq*c@PMxG^ntU&olHDs0g6n9_j{g^xZ=@Dzf1i68eOLB6O!-=!d5 z?{oc)9(i{|10G!2*&eim{kN)Ap>VecN&os41@y)`MUBq@Wg?Yp{Bv#8t1_u=ww48q zo;OcBp6?VQ<~z3$@-Az^2*^AKjYEx}eBwGQ49X%lkB!uMfJCLleC;BWjoR&%Bb_c+ zij;g9a;BxrlWtEuM)JLHD-Zc4By6&2^pMAFT+RJ{^V7dg&Pa_kSU97gt&Td+3T6(LhdgQ@J}UBU=` z(am>%3vcKNJIceFR<3>q=$03CXe_4)I9f390TWebERZx*ymlApUujW8>czs{lc z8RVNm!l2mlFhcx^4!jJuk9G@r)Sx5_fWAOOM$f(iu;MyHSyHi`3;ORRWfyF*cy#6C z3o7Wn8nCdi%(mx-_`X(G|BYdj**^l}$9a#XOHH?SF|0?cjMOitKajAhQ+lFnzB_*Q zf;*Ie_bq6j!7G?(K{#77FsEmBx0H|sh6CZ#q^@*9;C2e zLDFB9!U&b$eR(b|DATsJHB|qt>%x-sGuZ8Bz?=HOyp+WI7n1227j||gx4Fvcfz>%* z89g8lbtsyZ(F5k;yu@!%(bv_V20q?&Ef&%KVE9aE}FgFO3F4dXeEQM}6=6I^*MJn_?-t(m> z&uixVv$fCj-iMht!Ja}G0#y!LOwnR_64ZE5ucK&hmJc$zPFQ1zH zQQ}--HnEe*ca`ravOmsf=zJOXFH)QfrMX5J3jbJr!>vEfY>#FLXq!Vn6uF$&p0`_+ zr2mRQ{z(4lqjF00vC&DG_e?9sCYb1Q49i4zLdo?mDqWs(>;3pFMTkquN9YqHp_*R} z?2{oEKn9l+(-V~zYf{l)ytC0??9`9*=wcISHAspSf9`h9bue zq#QO<*8S^$ZuZJ|7eKUVUep%fa%-pc~beE8o!oYv%$vFM{FH!cZ&z0tlkP~0o zY0~|1bAOeQ{?OP6h3il$vLJ7rfA34!8Bbk0=vv;kf8AmVW`T$FwG-C7ZZUJ8R*E2?c2v%CB`OCjD)~rDIlb*MrUPyUC__gG&aOq;ir$=8?x3)1bE#-#0 zgg8ka%YL60CwOsAi^2Mud>}zDrrI=Zw~>)i&+I;fc0ArGQiz`tbklqN*`=}PPAD)s zECOB9Ay}ZCfcIaW0HOlmmjG*YIzPK&D9Enz;r>c_-oKs(23U9oFR$iPJ}TUVvFlaUr2n<=exE~J^6ciTn`dmNe*KMn(vZ(b;m+f zl`YicJD=|?c%Y~--DA;RVcc(Oz}ZLQ_=rGX9^0=%w}RLxZ@9l5pl9JP%UACAJ)||~ z&82)ytUh?s>QtEs$vqeMW7G{L<}ds@?44uOJmw|3L1sp%YDvznX30O< zkc8Ox)?Ba2yPvlON+9Ra5B$J}Da7a;dUD=&Gb2JZ@(C2deo66W7qBq(g0VwVSp6jE zSB0K<9zJ;JUD{_NX;C0sQdV}%aPAS?&ku|m?!iB^{l-rn9p)fCDX+#m=f4oXuR>Pc zP9B3h#5}7S)P7!}2}*-8)bP|gOwa+$A>Q0aaKSsmoSsj4#B^N@&55ZSeG<4^@R=PR zreO!%Kr7NGf2);UB$1fj$*`E5?>~lN?p;#*v2C&_+s58iaDUTUh(SL8Sya8KYp6|A zzwW38MG{l9P40n7NoD1UeW?AWd(ZCEbeX^Yc4ud2DQ}h}Y4(kme4Kc`GB#PSDxTZ{ zvz+HB4IHP`JofedWn^Jedg+$VwN2(+)qRaLy?^2;Bfcs2_HNU!v zM!>epqUpn~coNB%xSVDCtOH-J{A~?;4CZl@uYgte)pt=o2F=NHqq_CKl4ev)qd9z# z0@>`?dtK&ids2Kt4)CmAo_QmMdhXakFfuNIVV@ZrQdc>S41!9_a)w*ieT(K@?Js_( zD2@(cSekLz|8e#24H++bnhbM*HZ(H_(_=99%=>ejHZfZ%B~F3`>4 zd?BUCwdAH@rywKeEx+#}PwC>6@#zy9*5CHEmL!;-N))+CABZzAt~4+ZQv3stNx25Z z4kzmwHqv-n4u#V5-nZs$SITpWW%Y%-gBE1p`Ka;-Bh4t+-ypbP$l1R9gCx#V54^Fw zdaH5%V3MO1JI8#aJfrNIQR05&=sdZ>FSeFKy!Bj(ja^>`Zv$}xm-ukESPJNO=KOW5 z5^O%gq1;ziAEoX!ndIokgh#zeJ@YvxFE<-C8)3>uyKFuc>{Dv~Z%Pv!9ZqgBI!3al zO8L(qT?zj56$bulzj55x=U}86;hcYPn=DdwFO!}5PT$BHF&PUmR!g{5X?Je6?2cOj z!sDqox%ekndsWtqgME|!!KAJ9k?NuTl$L?+N9%jva{Dv&-k-U7PKZ;gy?9B9T&fu$ z&$1S@K(+IGTH*!CnyVsUHwby>ZeA$VqGHl z8s7zfk$4Zy^QWZRdnQ@V3K_B*sZE|w6XPk;i)wMg?u8TS4^U5zdj7pn;)lbs3CF9? zWV_o75F(BBu4eb31PT}HLgGf6UQbqh2tZF_+hBUK@ri=_TR_N~ey-o`*X1lj-!h=s z5ijIADkOQ8{`(~kM@ihveewG=LVLYdEUrWEZq7^3fN!{}jc6Uiv|Om4e$;7WIj?=9 zQJDJIxff1Dz0v`%gxUTQg{f2rmEN62=BHZA@qtoCom1m!{Rqm+``5==%kzJ-vt((L{8IqtQymA6fx8a%2R=l`)xB-;l$+!D?sRv6SNAF8*W!z^iUEW zY^RRj#`S*1R-ERUy3VS&CfaY18_JxOxBVUk`q*Hawnd~(&+FqD58d9RTkFkm)$IK8Vd zwSEZPb1tJ%LI{~u=*=4z1)14)hbp^ibL*uLX}iT!-ao|b|(<$K80W0sd=n6 zW$%aZq+VK)iMun$6m*CFy=|+3#sgWuGken|i=4UMVs5b1 zmo0){p~6$V>qMH3r@CL!`hq zT)tmg`Bn*Lk_z9MaThuDz_`%3;x$)*H}bLyQa%xX;V@s;Agi})T-ID0kj-2VOw!d}NnN5~H#Wx&aJ1nE4{}nL_v( zj^BUjozXTJU8Bd-7@XPJqvZEXJz@)|j?*c;16b=x5vsd@l@UsEzhIu=bJ&XvpdnL` z4SSLKyF6=o@y;ckp|_h*pJgH+5)(HMvhyg|uR=2A&=jth$9kT&LuoISgT9#=G8vn- z>BZgkJP`7{(OtMS6*D68;_IO>FP^W5B8>QIsFUB?bx`tDT6G{FC=5FCZa~oek0*~g^)_e4--#}rRbzV^3I zng@UsOly)6`5i$19Bk|Cbf1Y)%(w>9N_p3SUFWUD9oKpUBm%8M<16x)zXY|) z>o2Z8R!z?@i%v|;?Sp57|6tWx;mKqjAh95YM^k@p3-1+q`fEcr=*bc@J8dC7Ni+5# zhv8oBrJeso^M|3Qeg$cIIWWenGG%l*unTIfr0Zf<2cr$1X;ZTMFk3=y;%HT@L*Tzh zg+9+MTT7?#yh^R4%8^@@0F6!`X6|-Rn@zd+PuqQ(Dse!{pPkk5jT5tp)Mdi)P9Ob| zDQ)t`9JOUvrC}Cj_nAyEKFqERsj;6shnp2pJ#EIw&jCzGr$DTV?I|wJ{iJXaCT@mw z??`msbBDnLlViXyXf`Bz*TmP{&U=76k}0D-P2%$Bx-pQ2lvk=|D^f65x|_kCxDd>k zQEx@($C0hY<^Oln1N!-g>PcXKo*ei4dRt#U<034c#jx&KPHc zoGR}vP#Vl07Sq{_T-W9@VU&a#IK0nQJI9N=(Zhktqb|DHX2fl9P$2|Z&3B7|#w-Z! zgq!W-2b=Sjb$p#al-`)7BH@V`m<*k}e$MTAA^p(2?uR2qykuXzVg}pIx~V5W&h)92 z`L6wA<#bA+@3h?%uXMh`1;XOxpx}+eLFVXn!r|2A;P(3_S&}eg%;vI{V+AJmK}kg6 zV`MWnhwhw$9^}HPDE@Wf+ZSo}#YP}_f3cE>Rox@4w0RXSaQt^CO>T9Xc=-d|QG@)h z*z4zw|WK9;hUR#2QLWY3Lu-`4YL!8YqlVFy^>)z;RsgVP7yAEha_yT@UKOx1J|O6nh!^cCKv zq`iq{rSQP=!zgEC4j$@$E}C>((qXA1U<+*1__x`#z73G~yhNWLu&uAj&g$a7%-^6v zcM@`=+0MaO%6rHn=bwsZcrr-#NBZl8Ux%KIQ{GD(lryBSLN<~8CarTHv&Q>RGe-+X zZ-WcK0!3w;4h$T(f(HX3b58}?caC_!_P3Wq+xg4cy$lG?Wg=%NwLSv>l&datuJ%#` zMi_vNqNv=V4_dxY_!-dm#%p!r=e`~92(1z2SY$`^u_aGPO1Hr3bBvI8D;5Lw^YLIU z$Go^3YzO|Sk0p^MH@he~vMHj*=tU;}<**uY2UB6FcZ zcJCbJhgY)vxD7H5F9tIds0SeTYviATsqjS9TX7^hLTes5%x*I2-X>Xp!9s%ZB}?k9 zP-C6(XuQP9QXKYzVz-!XzZhrIfP%SJ zOmMss$Iw<(pvdWiwj$1C^C)(xo7CuO;a{btdm-UlEerX7WOh?~`#+okJhSf&+faQYJ@lYQ@K|f0@KU6GOh|U=^uNa) zKyi1Yrxj#P-e?@8e4R5|K4$v1*tx=p-N4dH+HkQYfzfZql_%X|al*?rnpaN(yuBgR zu?9u+d0n_kC=K90NBo{gmML-VN9`N3BgCI2XxXzSVS85LR~0R?bH__sg;&axGUg5H zUV@q$orHR23>(=?E&kLs=VHy{il>NF?wGOo9!L;K}nlj&r)zNp%o)lR9?gu%hdtc|>lJ_|s0|U!?_EVS3_|ZRytL zJ$vxOaxq_RGMDQe2_|Rcv(@hGmxeKmE>Z>PpHN|%DQQjt>M zAzmlBclTi6f%|>vBkjzS@DXyj4!@0;E)sP;3IZgO>|50La}PP!l^xw#`Y+rSr=06br1YLe{y&P|T=}J`P>x3&n&}YY&|CnE`VNq2 zVtwASOY=wK=Dw0IP-TU~R4(H}{+HCqamwa9N9s1w22e~y!K@j45mbHI ztfchy!rwuwSd0ul5T!p+2tAi~5h)k+uavLLpyArsMcXznWmYx@rhY+jiBKXj?>f^A zP6}dxgC=>{0SjwKl(?h4F@ODGus10w3Drv7_YmeHs;}V*b1TmIoZ4sdGI6M`B~WS( zr!Vw`0;v+ZbD4wGh_-g>3Fk$$j3|IE(s4rAFJp1&Nf+liQhUaW%XwT9c%14$(rZsD z+lZ{jnAgm9iQz6Z5H>T=imh;seIQ%?pVHQI&?qSl+w?nIAM|i+xaZe%O={Q3pQ`6| zgbVfoR@uU*toX@V4T@4RD9x7InwXgIyV0IUZiPeJe3<^9P#`qX=JvM`;|d-wOMU7( z9}8>qnn!=!UDM6>$UCaQ^fk$Pj(#rECA>aCOUOi#3=yqy8y1}&DIIT4t11M68lFk2 zbos;(*NR)6DQsLz*l~>-(YpXs18tiF>>jau@*zNX-@bLq`GyI%QqPvq-PxT=J!;wp zLpx^VsQH}y0mW@s;fMn|s?j>e5&g1x|4@bja`hG z+%fBcj1d$GhM=QRaA57E0Or2m{;}z=&ERK-n`Q zsygL}Z*ViQPyDh&R5rojiSb>-$oXR#ICUsT&jXz<^dQ>KPiSS-=$|%^qY$9lo?<&+DL(J1dri})jyf&z`Va106 z?qm9h0)58B<9GUj@K7U*(A7>_jn=N!JYRjOuRLS^WR0LxMkF8Hq^qKN>7Dw$Ta^uVgu$$!bWqVp9DkbbWfi+@~&| z#@RqnhCm{go+m?RmvF4J3QQFQQ%DfuMJC+ozCkn2k!_G1`4j4&>L(d8rvg&N1yCx7p<;NzXB)B6=-2A6S-SfM)-E( zNFwZabjtV&uR&mc>t_8l+PUPBI<8VDr_(B~zxry8ENShyc4JPF5P%J@MKp7$t|h2I z$KE{AkprinH-i~127(KhrpCM6lP;y_y;G?Db)UiK5>+(52v&ha|0GsBpBr$`*;;qk ztakP$KeHmM3^~%HuuyE`@2PNky~HTC=|Ew?E|4o47uB=Y0$vsbDrdfL#-!38NsFEQ z8Kcn)MM}XeG`Q9m;U=Hn5r33&e{ueSZ4=yIkNeRo64M<$wnjS>Vz7A9*e!L5QbsBc zBwIX68}HccR>O=5gn zACtc!4TpODmD(A~3yNwStja`YpdV)OaES+{np_+>wR@b`K)`RGU8G}sW(rFW!2v1NzN{b7&lDOFSZBp%3 zZAkoFLi(XaY`9QlW1whN2ZQ8k%44V}E@KYmZe~Anp`sPDsrPoDm{3UBvMDW9kD11t zCo@_<2Mt8`H_N(@ytQH!2uh~I-{5?I(&~GCFc%3{VVN?KElrl1I*q~Y<4Z8!_l6L; zW>)ZIA1xIPw>|oCjRpV(^vnO4HIvgwiGmSd09ybsvXhggz)cD zF&l)D7MV%B&m^N!qRaX3Ycn7mSs&9AVAoLI65xm64QLtHnW0GCxSQlHoX3JW^BJzl|_;7Sh((Pb!}}S+I&HWPQEy)h6{mkI&PT6u>k3S1*)7zW#OGp zD^}*i_EaltLw3WFt@`d;pG~IGnvga1r)HkNM6mcYujtD7tnKVuyyiM{LCEV?9WNHz zg-$6JeoJUm{6Ft9alG+y+Em-H7F|=X>u(y=9)jhNg_PYoEiJ9sPQmkId3^nn$rc)K z5{R(NfQDyev=>4A#%wo&m}W2AL#bblN|PX-*8{BkEgBjU$2N>~h)L^CsWD@7V@i;e z5+NCKiG{+fu<|J~*`!=<9@8PW@$mS$vXy5(5hSCXXnX<#r6WV+=WPDygbwBq`T9?r?LJq$LK~;)=FS`9dfm?6 z<&}a58<{_q$*_k7S#kL`^Ns}Bmh`Rz3PDG1Wg&5qXP&ufDWK4uGBK32o~ZG1wAt^E z$~Ha?eq`5CwVd<2xG`*EGPg}PeZIp}i2foAPi+(xEmC6d+BR6Bf88n<5^3v6Ifm`E z^KoD1aK{dd#{9PBs7|pnOk9^i*7>ZX<2ymbq|w(G^}ZBmx_0_eYLPyL_vNH{EoUaN zuUE!kS|bWFKJ5k&W40LtsHN7wFmsgK3eyFl`?1rp=BbrfOfh2uU|d(AIh8%{`|GA& zZi%XD*@wHvS!3v2aa9`a7TP|3gc}~Cn?PT^bEi(3=V0hxK1_Aw#-G(5<1~{8!o{zL z`c_nQW>jW|_gmlZrWmfM7#5zl@TZ&od)Mc~OmD7!@eT3ZKQVbes6K0Ml;Aau>0pAS z0%KvV!lp%TFU7NN&hhwZ=Zznc+oFa2y7^$Ol@D)jGhFtM_;nxQH2ZhC?uLfWGHW1D zN-7PCR*K_i{8k@#!A~ZCSn-;yW=799P65whAD(R7(?ldwu_B7-ePqq-KhGuoQk?%6L5@+#&7)t;AzF6^aCF;853pFtdF5H%y}Zwmuf5*ux=-kn|lN#l$7 zhk*g#o^aiq8`Z{Mmjqk8``?U(g{i1iycEaIzOE1?li#}y1jcWMy_=$K4j4B!d+D@w zS`@jZId)%rEG}Pl=H}*6j9C?m?417hYr#qW0i@scA~@#xsi_UOq1IS22xHTod2j@K z+~rUdD5#+XiM@%9=)O_?lT$+ty<|5#jXNBDmeT>x`&@^nWkTa;+P~^IJW$sD*~+xu z?Jqx*`B@n0@KTYw++uN{AWmSW{Fu^kx|f2pTcryo2i7~eQlW}!=x@1byQ_SRQj)qVR8V!GH#i?%R$+%J${$GqI)PkFE12CM5T zCrL{#thNadL)$c`VgaqYZFB(j)jUjZJVCE8(wTcyCVgYojM@8zQ1`e3iIU|FX`Q#j z;YEIe{=PFU?{DSd!yvGQGec-gU_U6j7M9reYUuQOf7!V!ll`f`Hs~DG%$Z*XcP?#s z@4UeKY++!>K)_&j6lP`#gO`dSPO%+W%ZV4WOF~RHv;^-iY(xttzl^w>42)X`d2T$J zWxy{c`!uR*n=Y)>t=$)Tsm6>heofVtG9k=~`<04>_m=sw3#L+;9Fu}EAG#U|-ODy4 zFOjdj7)lAB7RU4xGU1e0@5z^uKV1SqMTrKphqaTrq#K* zvm3nxH6Yr@E=WfIhIh2WbhB7B*NUsvQx3B>b~YvhDJ&h>m#qJ>yWYI1;J)mIED=hm zww-CVXK=kufU1FkK|Se^vz|8mOdG02m_nwM+D?Pgkk7;U7?#;F=gu@sUEO-aq^|FY zb=C;x76@Y{*Jo;9*x3`utN|W>L?S)xv#Cf)%m+MkIAo}mH_FpLt(WawCZ$G+)T!Hw zW+GRa&f4OEnLaf`*JYNsclErr%GIFGQKZpivh7MDP5NWcvETb!&1Wu@JM$l|bvmv*9F`sAd&xvO=D5oOUjYs~ia4w4m;-sg#BKf1eCGq=B7SJ%)Y5FBxT znRfL1l?-2HjP$Je;@`Er_Idr)>YOH*4f`l@TkO~YCF(Dy1^w1!qhk##FnxX63w0mZ z@Xteuej5m><<*Dk?7B7&-Bj*dAqd^Ys$|KE0S=>Vvbj-APanDN$~21h9da{Ueo^pz zRSZ#Gh<R771Rqekr(Mw+**rj@cY|D)`!hhuMDVv~Wz&i+r!~V;rqh{$P`Audlvdzvl|I z;hmTe5(fUwWZLxQt%B9QP;S_eny}tiG3lQ$h}rYD`vwiZ=)k!2QY4Mh>mSpe#=J{i zu8rJ-%<>BxzX#&}K!*6$OR$VctuTpcYZO*EZO*ozc^vDIYFU)lJmp%4K}75ih7ZTO zvxsIu=r~Vn{|b1WvnNu3)hOEb&9Mr!(5Sge#sekvm$@u6C~)q#|4u-V^zp7UWHH@s zrw0F?lk1hleDL^`iJ8Z0xXDH6MoUxH>%!KtG~!>(1e1z)#UHJoBm!fJr1};5Lck#V z=#Q@W<35Gb8m_lU(^$vEwd}NgrA&S2t5b*Tz|ehf=DKLpnyTt*61^-s63X|$@$X{# z0ZcD~xZ9`Hw)@EF1()?pwbSHVZ|I}LxeSA;snIL%+0+pew|~hX{eEOyyJ%nH^vkwO zbnQ*8e~!sQe`fRvdZLFH<5o_ub0BoO{b1Vb;TiA}b*;3j6XH>twQT1j?eeO5YxPP@ zlIAg6A;g3ZY2IYX-}|0z9$Nrp*x3)ayq2F;XWQ-z9Y6_Of2#!tj`0iAwK@8ejU);# z6FB5RaEUW2glX@|kmFIw1JRfN#{BeWp#l3+&v4<#D~|>xA+)CHJe;{#@bmBoB61;4 z4k6tzcTQSw*H@A}`ZRc{kvyh>b)j|N@8Ub*h+8Mk4f(MzNmrLnN@!QQLX=H)wP(Ms zRjFD`HHHET=D{FpT_41fU!tciB0RjC0PF83H?o^u8i5(I6$Z1s9-gL9vBdr17P;vj z3;qqS`VcvO{f2JkV}b6KzFXOal}D()+KW|u>W zFB$V~f?7o`xO6J|D*g579O}Ah7v>4k*K<=N9~qCyL|j7^ zDA^N)+-uSu`$fLWG);)#P*KK;F`;c$$UDpkK?C>gn@~whI`ows@$-UrD5ifH&Mo_t z0b0m%*}bl|oc5WM%Y*yWF@M6>ra<;jD=X>hOCL`dzbVFaH+VpI<#`n@SR~7p zuh4U$zTlLeZ89jHrVh4h#jTvrv^-kxKg_gKt#vx zRY&|9bKOYaDu~r9TkOIk$D{W7!rT2AJV=dY+y5!QKt~Yox?1lKe~pNwOr_QvS&jp7 z3lgeLDFH!g8R05OBul-EL68Od9&QogYt`s}=tEN4K3uX~09k0(A(&$H;QxTxORgI! zcmHl}Kk)f1MKopx7QA_G`nd`*Ql-d&DW1Ro^WIzPdr|xs zb-9KF#b%wd?DD*FWp@k1=DL-9t-i-o5y_L*f~%f9@HYcPbgtFtpk19`MBwghGXM;E z$J%yJ;cl@NkU>0xgxIcPFB3B8<8XVl2h5oGSQ3gi&Tw_i10 zyv4zm8qv0Pe1%I$-T<5jM6A-~V{l@0ID4)>`;^s+>gTPnQaxx8*pXj8u}6Xec^NmJ zf!H7mI$Sq&lf{mmQ64!#(kJ1@pjmIir4lk+QC}0n$luZIrJdV|C0RJTGan zzp1wNwfsp?W;aFDh53xFPC!eDB^aF55<32Sf)886aciV&+OF(U%tysUz}37STy&(P zBa3d<3JdpnaH}a5>DYNAJB&W6PhGU_KZWs2{4lE7S{+@I4_N4&P)QGvZF`cz9-8)5wdl&(^gPsp6XMIR2ZrpFYcA)rbmCTGfOb^I)*DwXDM^+}(WQmo zO)P?ZFZQrLBt_HG#z>Ba2k#CZOsy}5oag{lp2FB4w3(ncBpc67Er_p1&pmC=_?zwv z=icOuwj4GwQZE%8Tc-e$Ts57TPdMK0x39`Gw@;rXp*7`PU&f91qX&l5ydG9x+7K#Q z!r^iqFmWMKz)q~y%k&Mwn7J7qX`j488H!*$*Y z4q8h-!yX~~9w!V%}me z;r06?$ZD0Xb51$hjCMxEdB&p&cYnrE!-80LE+v{3d*LwO3xzwu@$|E5JPwes62dR1 z*cxcKbGtGzbDHx(lm2b=@kIgpjrljpE zeu;mEgyo$&7qvD%;ap{eTdNHP_%0k#N)TtA_ zF3gQMq3q&3bE#fQ9jAX_VBr55O*|K;M})%3vMX^w2hytXS|9>969W=`mF%vD6~Zc0 z7AXjJw9y=l&8!)|8@;^tXJeQ34NvbQfKU7&T2M>MuY&zY{_iR^!iCxY{T(KvZ_96Z zmc&>QklN^eR(wxKhTY=JZ?|Ne6e#7iK+rq&T!}sh8kX^o{!^RED!>q^RRTw1n4zFK$5^1;Vj|hSS`Qbs!&)NSy z!5M7m-QIiOy1P^z(OO(->l5tU_cvy#30dt`2gyzV%R3sG!W}S+>2CJ(idQvGI<YJvN<(Oycp{@IXrgF=ypIRQy4bF2@7ZQi7 ze8{HWHUcBe{?(z&+okY!b8<29GFDiL`*8&ylH1*UD{i_fMDF~{DRXcRdDl5MEn~BO z(_GOmbIZAR>7x;;Q8@^1RzwP3!W#^+s}~`!mzPk%?@S+qzph&aDQFSj^FyV69dt5w zbhvKu>y!4yJ7Vnr{tt%FVStBzc-CynCw9{N?Y}Sv@&EZWbAEPT*wJ9S!Ut!?2T-o$u_zTiTW{_Qx6_zwx`ueR3q@JRSDkTAvoej23s&0sOS$=M^CQk z|5)FNxk7dRSB}MwJ#gNvu=E(?l`^b)Z%YY6wRf&v$*x_{Va+EUfT+hMJy5hVMeh70 zp7|WD4Df+M0!i-}KIr(1WT28F22s!6vV7o6O^YWbW?66eAb5)8qwVt~G=T1<|Dkbo zEk3>R-G0R|!n)`6S@Faz!>o#yxWEHK^jC^GH;3HYF@5j2buX6Y*BPU__)_zI>LRC; zMat40>)qYe;wd86wdDd{MY0S-1k^m3~}6Ad82N3c7jY}VDtyW-3J5@>j&Ny zKlo@|I||<>*uJK#f!jpYB9zZZabnUbumaeHEfX+GB*kEwoORO+bcu*8 zTQL1?iD=huR2#oHGiI=JWiNev@lvtauW15Qe!8cf%LkQ9mpU>Ekwu zspc2zEB@`jy<#aPn`#sD{@2pdwU=HUbo?QButrY!kC!MBl}JjxSD@>4g<9{&={bMT z$YY{wo02hj7oM^-Vm(yO?9R0fR?VBPZF*wzwQE~igN{$hl%6+dIU%Cvrn@Y$RD>v7 z$5|6^0`rYzQU~AkZX;mQANZMM3fuaMcd{`S`7U1_vF0w+5#c-MxP3af?N7_B0GOq^ zDm6G$faIz`^(zxUk$iXZ9!P>!f|sdv;bXO!I{KIaB_DqL6~LlYlzTR6M&53P5WLrIPppVG$e$6%`k6xF zTBXaAUx`u)x9L1cza6fzeT!OJ6;sj+Ct*t7aOXc{**V}IPp#FQ&kXaB(w!m$+6QWc z?Z@NPW-fcg{+@0Ud93}l5L4>$is(9)T~H3fcuyzVr}TGb1DUoI8esd3Qbc!lR)xAY zncW+BF!c<_-F@@pWhq-7II^Sj1=17!tq#eq0qu07UgbjDO?-!PS(+WGF5M#5a596 zh8TzV=O#Ze9`i&88Mk$Hv8@dSn5uXw-mo1{_4C1SlYFh^I~-wBhE5)UKeLIuxWWec znoC(n{NfW00>A|?zT-*W?=PKl)iXqH|C!ct zZ1RrQiA+EPDWR*M<+ym63_0v7u(=yp!elb%3dVFf>AXZa8qIt^GEr#su9nk!L#&4K zePm!Ygb2pB8e+j=#3HeJ!=pcxcSaly`60B%&Wwe-I^CMa@+a^2p)|(M`@P+~OwA5hHt#1KNQi1F`iNmgtCEXX-UjMg>%TK^ zd}Q*%X)=r1Z3tdK6BFsSp>OuJ{5DZhvB4KD#BFp)5T%2XChn%tZAHOk#F*e17kV~=}IegEf3F< zlzr4E%t`vDcRm_PBJi|FI$#y!sFZRBYI~F~hE_SfVJ^Gdb>Zvb{H2IothV2aK-4I8 z3lFkceLAZwfmo>E))`j}*t(=xwB#aT#Q0Eg`E>)BqETvlsZ~@p+rmQ$Vzy-T)g*4| zc_sYlG?v84XTo=m&*1%-q(^))OvAR!OK0{6Zrzl6EOKAs84nMKbyqP2TF6QUS>^MK z5xe}6pY(izV&M_6pXA7m=32}J?6(*XI+!1()=lCU*78`cT4gRnjMQ@*Z7 znXb5K%bx5W$zcXU_0B+nGOBMn398))UGIgV>;0r6da;RgND5ndY#F)fAZt_Xeo>)Z zZz6p1>FS&MDbE@A)3L+3Vh?`}cQl?w8-5mrzWg4jnDASC4nJaffpypw5 zu%{WeN%VGY_N#fC%P*Q;$rfebnB({}b)jhA;%Mu`!+UE#yK%DC`Ni6jWOnWlDt5T* z5AU)Z_}G$=lr;D;Q~UFa*ursr0qQ*}v?m>Lb+|*no!-%Oj(iPt+&rI%~O?}3UABb*sf1vz& zU_%hK51fl8)Z%RSa3%J{D~MuUjoiK%)|$tCX}X-Z3_~ zd-Xl5tGOYVhH1I8`o-_;fjmiSM##)tv4Z-$Rn&Nuoo^gzEI-pROoGUmxnXaa6yxSmhN)zD-)ie6KwdP+UP@;JfK&2R3q z-*+U5{lh)1b1MPukz|oU(cTadhr0fLCgp6aBc2|5Ua-5xc?zfc`TG2s6;n?mH2-*=ibPF z=F(Vpl8n!>$-s~M%c9E+W&2Cj^{=f@q;edJ{A%eUR&?`ddnT_eSjrr9m3x#i$fR6O z#nnjexVAlI+RNJ8)-cF@JelLLPU%_aLW1PFjNZ3r#m9aW&0NKb11G&BPyjO@-yu0G zLcw+yF&X#saVM;SLByC&?*78;%d$8JhV zHg50xi;_OD+_1OlWN(KYtuqLm?Eck#xu!epV9w5Wq0;TjBxLQ<+z|-u@%`{w!3(~= zF8_NoJhVYHVuo1qI*L`+2#V(M?=;ZOpl(w7V1u9h`g=;Fmr6y2;6wYcxR-DNg^gOHRpvc&U1AZ|L4*yLz^qW2z zp2RP`Q35A9&-K<$OhR7cXeOY~ow%r&bK7`n#0#)8!5xWfCy#ZL$s;Nng+TK?v5r;= zix8$QzLh7Mo>9w*H3u0Zr#^t1A~b-gm@eDnx{5JvbuEHcj5WJ~*FbF!tJiyF!B=WW zKgr}}`RE2%?#zRQ1rg^{Fm<2lRJMcgM)3GO0>sN&h$JHKld9yxd~RPT2#>9!-M-x+ zG8C@a;V2Jr?U}|=vw|rxVcuZUQ(Ao&j&co;T=YN*++_-GT_vQ7=nGVO@e+yN%x4hsk4&Qh?U4o*rg5r&v%hNYG&TlS%qPTJiU9hKR?j zhwC>gx;Q|i%^hYQxo!$5A(zZn%&x~vL3%_DWv@*(zQ~vBIvRLe+AL*-nq4O5-62U* zE2XaZlqEO5>Gv5#_lEe>m?t*51mV+`qRd&%uU3~WuYNv=ZtR(=?&}ywvCD;8p-)hX zwYS31bGOU)LzlTiPs}w9GsEe;PzYvsZZCnh3ByQZC?P-e6qi zItH5Ps0-#*Y{7*1V8wTP0`0*3)Qjvr2QxEsykGCC8Z&rHg3}CR(OuLx(vuUle-l0 zLQ;y`bnA=9)@l8ixxMxA@{WGRtqsL6Ghg$Y?m+=~f=Cr_`~4O(9ULKTZR4$`CnwAK ztYvNP<@_ccxcFeLqg|{>unu>4b04Gk5sHuG34kk@^aKd%BTs(0FDovLbWk^X*VF&9(ajQvqS6}PGdX?CkLaHL_&E#`#;yCUKmHCA{Cy^+S{t}6boX;}w)V=H4nRSrtthCe7$f<;fiHCiz{=#oaG?-)vxMnm4yO}`^5;I zwAZb0g$r-`pluTr0#DY7O2tr-zoQYU&5Sa?hy_DG>u<$OiI#7TAAr{<%c-4_X+0@6l`vLYTe1s143=Mvr~^7?YN1ua%CQ5kzd5h zpU)W^H0NY;v^@uWIfEYQFjk;m;p%XRPqu1MzO3X{Pc$E;;kA^uc?C8}qDFU0pC2 z$(Nb(X#HdH=^l$0<`wz*P|%8l*GzT6O@Ti+@!oY-@F=Z#gfNO2ipknPl(UqmeMrvu zYjw4kiQpd$d+;;?*zV7OI?X?^^Hj@>?=dCe8MWAP>N??1q&y_VZLF+HCCt9mcImx9vx`uVQ*8AWd-5%h6FkOwwSY|8MhRqeAXw@E z99om|yy3lCub;P42 zJ_ML$bcpodO)A-Z?1BP9Gtky>-EeIe^a?5MQRXa+g1dWV^z*NX>-IY_bDVm#CsJ$_ zA-^}9CAEDU(STXcc;kKUL_vTOB+MF$9X}H8A0TG4Rh~p}ioH##$W6=*k72`9saCLI zRqC3jWF-GjC{$-`bm>ZwjloKAv)5GF~8wKbl{~WcY zBYt_u0Zii^;I?UcQ8Yr)kQ8K+goCA(@T^-SZ~Mp;C571KM#${w0GnR@yv`)jUKx?Z z+{(R=PVtvk;F6}Ebe^7q#{|E*+Tw4O+mPd;-ky+{Kz}E_w{_(>lagn|ixQ|h* zT@)VRnA&%Bya%^n^|qTcEBOJS4J|3m*FNzf3~QG27&;_7SOSHk`r>;dRX3K_q1 z`*kT)IJa4T(MPk|F*trE~36n{O z(FAGwU|Moplb4t2P$yAu`aeR@K-A!oX#tHG*8q0SF_|tcZI*`|I z6nw?sN?8#MAQ?6Lsj{~ZU7ebXLI?Kk86S1?zxO!L&dOiuSr zul}|05F>O(mPsN-W^+M=#9ngpzvb!Wh9S`FItC91XLv~Lz*G!MvO4w>$*QV8kt2LX zF{+S`02tkp<{sm$m5N=9Z~D_6e|`4BXVeI7Xy2dgGl0rS$TuGfAp4Y*?a2sEqNxJ5 zIdN~@i64lSL|7qODpNl^&f{2?pVIHJC|Xh5eGuA_Wme2BR4fn3qmT22 z{eQ~R_SW;70^Aih^sG})Jky|%xiH$MQJf;OFRPbdw4|ZLDC$-ny-{4Pid4u5$4)u+ zA44p}PPxDR(yQK@Tqn$Hpk`V}2#Feu)_y&F?f>)j-EmP}+xO(>i+=MIeZ~TcfQdDV z6j6#a^NgUPVhJE1MNo>A5s_X-V**N5Q9uz;5RvZCr45EADj*<3A9^oCZ^O*|_POB9 zWgyRg!Y7HtoO{pNXYaMwT3fmfqt36O=(BdQoJUU1GRnFVODAmx*&7cYpZ11I`H*}qzQoDAnEB^57e`UvD$ z3V5S^BO8T-yrU>*imG$nrt;Ov^T4dMtRyp-4b5QqfiyekvuHxAh%7oj%OSqY#=i=l zy}!asZzH+!?A4qCL;Q9a!+>zDB;H_{r)}MWw8ZPa?GoVGA$_IKN~c))@^+w+`fXBE z)N*njE)podqQo%!eNKo&I!$~A8LbDxRMncSRY1)Fgji0t1s`cpF_>OFuM)iLHl2^O zuYrty$nCx}XKE}?axO0QJ0fIsUCV#%01lMXXQlyvQNlXwNJlFyueH{Aw-8plG{_G>VG@uX=2 zSf2Tk#yib~ov4l^x13O1Qp-R|ZEJ}R!6!Q_3sM{IHJe)Q4|O^CTJ7nN2qSF}2mR`O zK{dshO^kZuLqI& z(E5>YFpE&h3tD6c=Mt728>oLV!A;M>Uio`?<>iCTvl@-CpxyoJwkgVEdNIq4E4Vms z{tc;-cIsl%+w0f4Y(MEd1yZZDBLi=7XT8S{tQwRKVZbXi#}*?s@zLfJeL;$r+9LXI zx7WGH!@!tHdK;hqNFojFEZ-j%ivv)OgJgZgV92ZN`l94@(a&?@7jf+_Hy(nr~M zHT*AoPlt9xgFoj0Vm`p;qGcFuvwr_QRDjNfVHFlq&NnJn1D}#}I&R;DmJ&+`293q( zD9C-g-1t9#ymPH5AHbOu(eVvS;>#05{YXB$#)JbM5@8{R#NfzLguH+s;vX zq-2^5k(Cgyy<^uLk)FBDfucn!1gwwkcgGWtnUA1YXG{es(F>G?m05##R~91ta&X0} z0Q?OHaICBT|AJ#*9Bvu~{8$)blNJ%hjFc^sNHdbyX9K5x-2v_N#hYIf!NJ*lAsV17 zU+_%uOafzi;2igZf{j!xB;hhWd?9#TpRCyV_>xPF?n&u^X-axg8K}K+*?oM((menf z;m+sQJA8tD0pgCz|tsGui zO~eSW_7#Qblo*vJU0Vj7eQ{Xf)=Z~0V~^s)IQ{r)!#)WA!w1q`s>7;b4;WM^QGm#V zD`_=R$&Hqe`fI=#5CYYMNcM{35c+}*XGS|g;KaLiBYiMk-%MdPT@GwPrt=-UcQw5& z0SV<#CSf7?M^MN`kymO(VNa$BT-M~ff-X&dIaVTNVb^j;VnnAIw}wDv`y9(m9qI8K)7 z0lz{C`e4nDuxj|TBW%oXaaH#Y@EaD^RYmg#!z5knVdR=2HQRB48Gw24&qkq|A&60C z5B(-!4WJ|s-&~}$|G_3ZqqF+rUmSAn3-waRW_sIwS(`@_9)Js66wQ^Xzn;oLF?fFd z`5R<)pevi^N}9XB=M-%%^{T&2#~!5-`cLfBzp(&qedr&9h$=0OKc`q|aZPV}U_Xq> z{Pgi|s5 zik#>nYbeky=3EkU9$CFfNiXj8Q``!4>XRHy;fvD}8TKFZGVC2jJm>ywAdiJ)j5#-} zQiR1VE^U(qEC(_b+*4rw4fV}FtRp#ON6g)aKYd7dq{P1!B_w!>y0C4Tm)1$)NG9S~E zu%g~GX%SWnr$G;*e-sw|I|Clt+wOwse@42ng;xsi8$#jkvEtuVB2-F9&LAt$>DrFn1NX}fXK~SLy>Vy#I@zl zG8N7XTYwA6j`RjZ)kVc*v!ZXt@ZX?UpXlk93ND2)ynL0*AO5qr*e7mM_WXjI*qqn- z2<1ZHe_hu0WA=B0=hHJ_Q(8H++mi)?hjN{9*-lCVH((pT9r3}dV}!S?xE+eMl8PjX zf1@^qSmb|a61qhPEKE7{zPx4|C~oW_$vPYrCWOHnllOqT&mhlMS7xza@R#ct@FSWj zvzm}idW>ML#f{e#cOCL%>U6#u@mZXbX~8|2>tIZi@qVQOvkoe}Vx=(}5Oi^f4m$f} z?Cc}&5Fe@fH^XZ;UU~n=_w(Sbe9w!!;TNJG*S})Dn@y=|i*6u*p2?Qrs5&qy#==v; z;!C;0-VfM`Mxn1ePA~J2Xt2C2MMNjSBL6HbFp-G%0y~=N-Gg+GoJB8I@>NO5P8zpL z)$l^*GS`o}KgQjA8ttN)g#PEiSVc(I<3%QzFIdhFVC7FI`R(dQyo8Ut0t<#oMyqG$ z0LS{T;uP?8@#|(met&BL$r8dOaN{ZR{R!H^SF{1s43lSW_tnA0mxaV zcd}s~(P0WZ+cQn{&nJKWLFdQ+c71+6HRVCg zuJ+sX*VTl6*RszH=Bn`#i(bpoPyLtYn=#ve4%eP!p9Aa%ca_w>Jw`m@+Y;Qm<=UD1 z6Gh^c9H9_@KSdT6G%9+G3op^w>^`ZjdymBux>a~Bo@dTK6Y9|OGUMv|KTIuZ&(*v0!TdxLE4cZ23c$jT znB{)=z!{G-59X*I)l29d>W*Lzq!30OSGel{bO^+MW6L2jiwTnIC#fYLl6pzSZ91!j zFZa2hQc-z6n$?VkAVdlkwi!IZ2v*%zg+^OoQKRLgOkD#2M2&zZb zHHtG=N6(gD@z}MfCnlbRo={}5AfF$LITdMHJ9ACUYX>kH9i!05OUzT3{b`Pv>tjyO zO;A{hgoK8MiU{(bO@|Lh1nN2`@L-d0o*b}HP<}lIl7(k???zfVK>8{HzVfOZ%$$e# z`vGV6XQChSOurzDQ;^|7gpDVS_QkfxRH=y(jz<2NI5(Nzu0ITNpNn;GVPj#Y zZHO720U6*}_DiH|$Gh&LGzEo74r*?Y0>fDxLE>-rHBhX)5^aGbHiZNIV*aOK(7G!s zWHSQ>iYq{HAV%vYJNt_;+9Nz!n*eU!&_{hBnkW=M!#t%Fe z?^FPX*$kxdL#tM)HVi+4cWjuhnVXctmMG;lw~21$zrQlf<)GX6Y>i5C#Rn>tc81`o z07QRhKD0FKkFs2P#h|&mzW%6kw^T#HS~@Lk5!EH{gKazTbjdRxHBa^XHCScIOoAl6 z5ys^>-MxFaL?BiB0LN?n!zbs89hyWUaTJ-_VB^yG&(`P@U18kWPdbFg3~LE+HG;Pv z8kRc|Pckm_ydW)4vrY86dsZpuE2m36+|5;BU478rzFX1lJ06{`67X(Q?JD=%BkcGp zg_j=ca`UrGP1Y$+T_G#SakY%5Uwwp;bWz#CO>{o-mclTL{7m&Fn|MtV{1>D zgmIo8;E=en)Ywd*3mSx^s^_v=!3$$9aS>t%^a#gvQm?BAq~|$1IX#j#kU5}}CbDwn z%A|?iYN6Xaw%t6DCbZ*|FnCKR$jo$HkoJCh=oed{FjdQDpth33XO-7QFa7EmDCyN;UgW*-4^+2`V#Uq6K#s5K?(tplpylCiYO^-GZ#gT9=((bKmNL+U z#(ZJ(OWE(I0}4!kR7&c@jt@8sJ3e6T8V_5rbN7L97OAq%lNM8f77y?63bg(4R8qC+ zTyM`k>1KOm^*`Uabw32HGteE1oG{<8r-%goWhuFVbM6`jJmNRkK!9)4%-JUd?o5?Z z@+L!#L5#ZcfJ!SE>%Pnhzlf&S1<5to9orOPU}t>iook=kJ`(4OM_2y7&w3pAD{JY4 znz+Dj@&VR*q0zNumvuu;Za|Jsu=r1UOdrB;smYSg0_;^y3uAHEa(q~)x+XB2)CAT+ z(@=#h;;|WYz^ID=CfMQL{S@#d?Mjxyl=locg_zM^tXsPlrd?l8?O?~G?PBPRPdF(! zMnpyJfqVVa-F1rmXfgz)E87w1LD3M<54!aKCFXs`ye1&C^H6TTFGjsj6q}vC;Rh$0@sRXSJN(vKS zFVw3%cQm$0rw`#Sv)iomXpjPIZ}mgLq}f37X=-oZ;291C2p?u}^<6KFrl#1GABb17 z`}lZ1oZA5#@Ff=3maJJ`O&-j4M?dT+kpl`>iGcVvbRnt+T(v~{YZBZD*I?lyGjLGU z!J)6uJ?E^UWX5^JUq&Gw+X}iGRD<4Jj{SnKP92S&tbyBl=%IY z`Bc{MI_qI)>P2lf!N))qw0-ru=Qol%4iN6c12!hmMm;qPn4sas?{b1wL_>K#u!4iZ z`PnflHeH%?Ilv*6Rlu6jZ6gp*=v@SObWA0dZc}WnuzT&%LdE# zS73FA-{Yr$|0MKn(_;S=-?}iIwS=E%?y!VtoN086 zZas0$l}Nm7JKGzX((O9)NrDs``5lP32qZuCHOm}?WtRlQiwa0|FGOG#w#rS-R-E!ejsjXz@ar`)c^T|^*~#p#Fg1` z+!bbVz`o*W7NnyIGM$fW=W31W8l1x%3~2DfnMa3CDbtFHhPN`TVHP1Tz71;ZI{+l{w?bWxQ$8gM~6yX}wFhV|dAddvwB2IdP>t&aYi4C5-N8J&S9G%Nam zNwF$W@o*3GV}ZU|nKxg2$d$CKbhZ!qdq*Myk<5sZiDyhJ^H zwc}HGi3!*l*X|+ee|w$=4h~{YSGCA)u5kc&JEDW`xFU^${5J)pEIv>miM#yy+2Q>< zeh*v!&R^?w2BxM)3+UY}+Cr-G(!Y#be(jIdN%*73!{fbj-dW!5;7T6iPzBh*srt!@ zKJ!sF6EJoi^(Pho{Z}Lh7jgH>GJPSaV8>!umu6AbPtiN|Sx(*|^6&5)|7)XidZ1F{ zt&5QN``c$aIs0>>4ffuV=gvs1m}_(jv$n*)y%xHkBMk)odUx`x<&XQ-UK$PRo}r<# z$~y;At=o%`957kRr1aUQ_F}*C`d2vzT#MzxVKPp%z@!XwT4q|G8HqdZZzH+iI##Gb zlXY|@Pm2GYiGHIBm=R(I_^Q+ZW45OFCmNB6yVhENpP&6>AOji1H3=FLvjHLS`3 z@6alk;)mP$&0VEElSwGfnJ5zo0h|B`D!-j-_K~<21k@DzT3!iI{H|OOYO`~?evx7RVgc8AM8fl^x zm70X8_qqS#Jg~mlg+<3FGV$k^RH~>}nNQe-4|}*n7VWGX$0#M7YWQN?^U@k>&{-aK z!Y*zICbRXW2TZ~Q_a|oVbGGlJIv2+Q6%kjWnF?A(c$CNF=63ju$PN6*9~cLY_?>op za(o!foda`btIx0Dg-t&R1)H3(<5e>TOjlSE?!J}iE?^HcT9jf|)#zVQNr~6tlcqWt zQM#b>9}a^J&HX1+(bb`n=xP})a2+zw68pyU`2DWU)$%u_lsua39@y4JSpOkvR=dL zv#FOJSR7%e-tZz{Fyv=G?65AGn<34BSf=4ED>|xZOid{#g^6rdxnDV8-zvW0d&Bh- z_t0)ZTa%DS$>@uTO`5Z$M{EIdApj78@hZ*&wJE>ky0zR{fBbws3i5AHcXj?MiWA z_D;Roney~;LI41squ^nt%E|sl5!4a!gpjEG2*%g#l(MXUoL$w_6q5a&PT%`g!rl!8 zvVu8SdpI)7Mp8n^{RtJ@`1ji8#|Dbrzb|Qwi`G2u$Cj2XzJ9@=9)wWG0A=o@i)sgK z^Y_Ed(IDnrH!c(0{ynds|w;+kr@48KCmvVA|>-|$Dq_3rm z3bYqFm!U0H#L(2!(k6~VM@|AvO>cQ`xd;axs9XO8N6Eup4>+r)rWUYP{vwQh2A9?( zDfPV}giC(qhvo>9e*2~RkT7$Om!quiRSqn3U!v9)r zWNPpth1bJ%uAS6vUHpV&IdY-|Od8fwnjB5$+U^Q}rpBKM`i~f^NIJZ}?kTP)mzRX> zwK%3fQB64dcf+FhtD+XM*}iC4$d>&h4nZXYeZVr4h9*8Pr1_e)cCbqc@NyLabKrds!`Qo6)R*|(=I%-Pn7kd@&N1Th@K7bhpz zLqDVetfVGIzlCF-;m_}Utb}7{XCFzYf%w1(*{#d5|112E;ztK>lE|nqJ#Az~ThTmo zR^FVJ1IX9uHOp6O+fb!uR0Jd0(X5;m<-qav-k+F#&>UH>+=Byu)v_{lD7z>8l6BWp zc zmBzx&X5M%M(gnD;ob3ZOvwxmcn)r௦%NMnA^i2w{m~6Xu?L;St*jcBcnbYB;4ZsyUFm_;*zX0b<6@9Rr@3I z?~K#UlF~_xh}dCV9vTv_LU-z*I=6f5d48_Xz05N0zCV%PfU91P{+m0%gG+x2QmsQx zrClKg62Xk_N>$Z_ZaFuNTYg82=vLEf>wpPD0t2N|svxlFu*zkM1IN^# zKm+5fR;N?*?WtQ<^w&UaUb23nP!}+b0Z7JS^q~sGd73~o|0)L5mz3ro|2dhBN(U@X z#Qv&+fR&Ms>QMvko=m-oMr;gh3EhU z{7&RCIJEbme1=~9EL1ZQ<*5F11q%{0M%hTaQ7cdn3WkR84*@_}Q*}tj=Q@O$Ke+=Y zmaacrC{|REkg#YA!2Vwc`#dfhR_>xRkTBuq>@53A$Q7Mbs2l3KYjTtg?!}-Y*ruy9jp#HI>0uI>@H1M>+ z-d}L%{#DyQ*R7}eAUD>0e)ANh!OV%oV(+_aaX{WtQNZCes;6KGmatW>th2oz2#C7@ zvcxBH6l?~eRYW=!OKQ-g^I%M>E&^v>Oz4h@v3?}8devZu0qA#W80OO!2T4E!;=(eo zeWC9#jwLm278GbBKN7n&*y$TPo^n+s+4V0V^HS3%JlVleAjz9Ms^ z76z)}_x-7*iM*^LvSvoFyto}k5n#c7tgBKgyT?e_D@TvfSIHg^!SfEF3hjz#;8%uB zrUsfu%AGVmE6@jkQdZZ%-b+XOZ3J>AG$tl?5l$=hym*7AD6{_~{P3mT&9+R|x=PFB z2_^B&bN*s5@v}(IUkl6Mx(x6hi$+8Ow@VY1a6oj#MuX)^k}LpvA+e~2HAvyF&5+nEXmeVgWPHX9 zoW!!oUVNa3$5k;O(WyUtY{d%enUi~t+uRU$cBTgAZrt$t%qJ>aU(1sg+R#Ihers(M z$CyZ5u8y(X53Q1b+(FAu-Z#Iu^$>`1cHl%mDrFIgHaxUBp%1q3=XN%5NEu&+x8~Wt(u(k?Q7({sX}OQqw#m;zcoiGZd%vm zWIj1$qff7|9h+`X#$`9Htx}m2@$W{8_8y$G@8ibjUf3!)(Q-)B>WQ9REe2)+dZSOv z3#?Rxb+w5yz}oBZ0ioeyR5G0mM!f}JzMLCH3C`$B`3WmqkPc5BCl%V3lIe-zzS({=+7;ZZINK$i@W5-U4Y6s3mg7gFU0u82=o^x zDW%Cvm*t6W@-xHvu*JK13HP3|M#Um0JV1WL)nv>201f?=Svo!W7VPxoTP?j$y}{(R8IqxBc{SnJUcdu8adI&!}D_8rS_^%bbc+75hQ zbbu>oLx2nNH^@ybotl=$;R?uKh!qrujUKyYI5=+n7Xv5H{;uD{E7$z$nc>a z!O|P31=cqx((Aa=n@}hP=7v)feLQFx5h=XTkJ&tyJJ-2>MCaA}HMF9dMWAdVN<$=m zv`W#|LO6V_aj>P~D9V_IZ*4rjUQ^HU8l}rBYj88~P_lr(;XZoBv8SU2z>o9pn0zx-8`yj2&qtl4Gs?Sm-N6?%fA`Y;%BD`u-ZMlmQG?EoQ<;t7`$9gI?pji$ zB|i*9+`$FZIRbx^QQbe$;S(AvL_1k^(2d^ETm({`WU215kl<7m=Xa*m>`SZ`)KzX( zQT#D0w`~(XrEBEIDzLVq=XZH0Qkm~p8C6IW7kud|jkJ^*&lmPZw-zq4Y2N{SW>Z`a zWAYM{&N;1s_w;_zK-))UQu z%q!gyzMlEa!Df7HyY^;D8uMYq8ie#}MLhhDE2XTwj~8#-AfTBjYMCzt{1Ok>0VDj{ z|AMt!l)ikK(dI+&etGB4z~Bz>Oei|@6yIE1lNi1?=@uYA0c9srmWQJ$BY7aYQ#~se zpZ(pFQ`w_`uZEj{N{n#q|6y+%a0d=v|6zY|a0OUZIU1#CqxK@p9qT^~-)eLj0}BI~ zd7lI3Y{=~v;#=oXp;rXWKTO0nAHm9rv#_I@I6Li{^tUl8MNBpEd2#T+zKbOKAKyn#Tr|5nO*UIxr+*wVzLlzdhRPq$ zDO=|bLx0)2^l;5pOK<`%0)Pt2)^&a`RpN5yjVsXtM_>@a#UYrj!innVc5{cfj_C|= zyG3~H^8gl|T&&y7ax-IC!i2KF0`;~c*GCS{adbbHrNR26Mu&Cn3sWIYm>hZ)V(*K% zA2@jG1km*Qu{N88EiK1r;MGa4=;qGV-hqiF9@$elu=cInww>lCPypmXz=}7BX%bid z{hbIrPBO$1ZqeX#BZ9eoX>f&7F*gD!$^&Uj_e_<%_uC;K-zbd!&FZWk1L zn%TW)B&Iw3>l##CJNHW%H(26t^h1z=)I&Rm$Nc=(s*V~VP=EX ze#>LffGv6m(jlOtrSSf;w1)GRr+S}_f(+g<0)wNO0pMX(FxvTP0hTTD`BeW(;dwd` zwZ}XBBduiYM%sdsK8fNZT@shl_`{1ji``iK<>mQsWdaNuW(25E_eJE6KI!QjJitl* zkH&&Q>f_EX;Hb?bG~HotP6LOW?QnPbgR>|*7z$5s%mhoMB6r>rIjSHhy#v;ag4-l> zs+i_B9#J`qE$KT*!t=Slg48?#Wpi;xJ6I`o1OPow#v zSa%mBHudh*buQaOr!s4BV2!Cd);iWbG~6x3Xri^50A4*g^tj31PYVh?{6h!;PtCOD z_zDnWG0dd~a__ z<((*ujz3Z)0QcJGr|njcSe^A?Sms8Y>4b|7x_1)&O3Vn{N&m8s`(i_vxI4tSFGHwT zg6frHuh*$Ec4diO=Jxpw!qh5LcPQ;`5y<7{2_0opDL zuDF&M9*dYl$xt~jhM{jx=sR1^jeotU+BEzT8kT0Ogf8{Df5omfNW2zZ{@cY6>x{`{ zii7}XQ$)~VpQf&@dH`9S2vdPK(TB1K5Y$`kVOI0yDH=I`A*ZT8#U|P{!@b$qjE*w2#+d!1*tCXK9j@XE4a_@v6zBIYA~4zNSzO) zFGm-g*Pl7{6^4^Ha6(v;HD43Ra4)St?#@!w1#uFmYJlzYIKot_T_us|it1O@265;W zwS5>X6F5wkyuOq&15>gF%^0&2Bsa$F0v_kE59SOJ+V89qf|3Q}62#NyAjG^s6MU-a z#%EYc!(f^Yey?v*U(!}bw>g%lr-VWNy-`VVq~L<|Hb<&s1rUG&IL*};f*2!KbnCr) z)1{EbBTOaPUVnHpT?NRV*EoDwo*7WT+Qu&nyl*FmfkUS?t*tO}BE8AF_)>=Hd3a`G zmdC7?N!=3B!6GQHBRl1UMxq`=wf;{QjN*4&rVKD$zUJC{t6# zhe2vOIn)8OsPb9uO8C6(VX^LbfELz7CuPZ{!I~D62u8C81)A^aGrNfa6xuc=#mz9& z^z=aAKFecUmdVtyu6%Gsej;{nUx+U7-Xi(PzD4XEk`*;&+TwTw+RdZjLF_gaA^ars zN|dZk+pb+yV>UbHXsiuauKFt~DgZr#Jt%&e9nRg?om6M*e>)Km z!EZFB%5coc?;3$TFgIodQ*!XfTMr#?41HUYuD}pwd)>M5Qi7;${wGkY1~LEOdz|@l z%`&JH+S$?2Q6roVmMR_Oj|8aco;l0~HXr)}-pD2XdrT}VGlz7Nb_NguC9(?o(j-Vs zzh#Es*VWwnI&T=p3}0PydlIg&x$(yr4JX8ia8sKAj*&g8)9mRa|jIxYP6CrA4W!u|r-Cc8zH_H^J;t@^s58 z)qYL$R7CkS8ZDE7tgyTVF#au) zCX0dA2Pzd7MSkFrC^TE02xT~wZZdtb`p93e-aToV%M7eDfc3dWAsdH4sUgbiC*2O= zGfxlhTB|tSVxgej0Lt(EkWYnwHQ;rX9SzI<54=|!Hupp1d((_?Fg&!D? zp>SR%OKNZRz_n24*Zq#>xxZ}^jHI0H$uO|NE>v0!|JwZ^H$^qLqfsA>*bKVA!G94! z=tDanj52FXHa%~~xDTG11CfEsSo+z7rVvHRz)!rMqhYz!2+^q;$G^3ZvI3T?>&=gO0~G_@Wj zEq?)-*04l}`Y1<^6t%}!X6!TWl)tznYQE2f%iP?@Aptz|1Fi-c;gTGPF&0&s7PqxQc zap22~TfRYu4IkrkT+Uz`zf0TwW$MsMDAM@?A)-Uc*azha9EE_)m+Ba*yts*|iK++b z2oE*rn!*jfz!D(#LB4WN4x~aDO90d;zzI`8v=uidb1E?NN^oygdn&k_;}3F1RFy&ts0l=*tX-s3{x|P#@D6fm@ue6RXO$Ny?b)BY)K(N z)#FOv*>oEZRyaX_r41#h;dLT}-y(k`fTo6D(#-{*o%<|#(@$H^+e6>zMRjAcgZ12; zJ}WiF4b*<$^XYH9XxEgQ!hx!GoSX*~u8jA9le$rpW%@N*8T8B$Z5*Grn1kzezZRl& zOBi*1egnhV7B$TtC_^kq{Qv3vQh420W>@P0(PFkHqHXFJ%Ws*;#@^^ivr@!v8~$g# z#GykQ=RwxtycI@3YX^`uG*OY}SW%ln`pSzmM@D^Pp_&{toX6DUfAq^k6Fflw;07Sj z@vGB$H5Z@SPu7hL@R2icik%aXx$%pd0}CgCRj?khf`U;IgL;?Cso z#NYTU?ZV9epaAdVBLIxtFLKRW=sCS-Z~C_F8~<9V=ce^rn88cP0uPEpni*D?StEXd7^t^dRC{B?P==A2fVS3-{H(FXY9*Au z8kw5z1whe%A;zs=zYNTJKpJSfjf}g(kOqTf#8Wnmei^rNgPlvRazuvO_XJ?TX`PkQ z1m8j=o@)l)qkx?vm^qf1DB`*8udX9r#ZdT_j;u}=ofYoZWLuXd1~r^vu%O)sT%gnT zf?FLUKq6-QVxo6zYM;9Rw4%=Lv&~2K>@C5YC!%51sCx!}MyGK!uy3V`5j0F;p92Z3 zs>(n6yY=f~G=iUxfV6SHkFb*BTBW&R$YRPrhA~6}a_CKwR{Tnq+Sx-eECL6t?>N4+ zbNG!3cJv)m3V>ySM4U`(mxRY-IsFHXRqP>oc5g9cDO$y+SH0d-E ziw9UK$e||*p{qU!2ik3Czh}|87tcn?cDRx6eYiCdpb<$LRBdGOpD698Q z?qCO68b`!BAn@c}d^hEx1lW=*!HfYqNs;OI>kL0yWj(#u;HG{5IF+y7qOo(o2n z&QtfmCG`iOX5kQt_!4L?)Mgl;i6ge$Sth4(S3AGadZuJg@iPGey!ezA>%0Sr0P{s_ zs5X2N{fBptX*1!1XF5M@tcQtbuY;{V)<2gtISX1=W6R1TxoNcA3ebN+oS*KJ25sq( z4ld5~n`K{OPgOzH<9z0h@#4<;dbo`RPb2^Thpo+7QKOXrqoZOJ#}8Sv4tD%Og%Q|b z6LQptIayVz`dg8X5r-{*$%K}|5lW+jQNUT~n=a-$=eK7f`~ZBFo&dr(JANs{!5Yo7 zO5q*ENZyNdO9od*jmIfmJw%ul@vJjvRtao^JOP4}*72jueBaP4wCcIjbzJ^h0UEWpkqH2%_@3#f zT(h+E6&6+rwecnI|MnYr@>mfFHJb8LEAIoJU+I0|9s$gl#h{FPF#&3+0OEK=KAkvh5!%$bXavJz8x5$}2%;uLb2{mL8QVhhNa zQc*%Ry}0w#TX=o12Qtm^FT5V+p!!xzH$KQ7iF2Luk=U?%acc8+LZjKiXbw;{pyYlk zlM~Bar;ze>VbzP?_;w|L);%z29h9gdg33|hdksq-?a~Scb)WM9G>RaXac7z*`j~^& zF32&7l;k#AVpanFbwA>M-Fd}lc=*jApzmjm2)DlvW5lKKHqcTvw?U6Yd2vXOIEi(^ z`FMAH0sz^%(+n6#XTxMA*E$u;;Q=@bJ3)P_Xk)w>l+hvN$Eah`?MBsW zfYnE#p+p7D)r?_xSVks@iv={dP^(d5k&wrK_Zd&{>)TBecS4T)BVbz#FFQm|+T4%w zwik>Tq-qTgcn5~q>-{b#mu$xduWnDa!1^uT)F9q*iJYWy8SavX&ok&h zBxUJ|_2p;jsts@Hz)px`P5VJ~kNf8|E*@imVK_T~4q`=ti>X%dDj^4h(_xAV=KiAy zXMdM9@MGbZbJ22oWW(d$Ph}vZWiLRy@_*%VFwA$4(*0{to|`eixu7G&ue zRV40mDEyx9w?f(SL zdF_4xHDA+bTtD4FWa03bJkyqcEzA> zq~?=*o^VNTbNBY?jkv;s%V9wMcFsk;Ty{?P(T-!0>eeuoE@)61Icoa1Eyc^nf zJNcI|1oA#~l=j3!v9iGa;&;W^0U6dt7(1YSIDcI^j96$Khl$pJ^*{%*ca`8q6}Mhh zJQh_^z^y;VupDhm3)hj$d@vd}4Nt?mPhQeF#dM9QqDC zBIL@RJ;9NmQs8ZOLbgqbyYw(y5(?JEynwXNh$Jcu8oy|Pt`M8Em#<;y!=Bycm1e4@Qfx4U9(;9%Hz>bB#Ei}&R8xT`w zZb(JxBQ*pncL~64aLC&W1{n7=KijlF>6Y;}mm7<-HMQmze>IFT4E=|Iv@10ce!hrF zV8(d3T*d;**3@{AB2p6d!{%$!_dUq~No7DEDM?CywlXl&aoceO$7DlK3lsrg1&PFb zUJ2tiX#TOkI0RwtXokxCc>@`K*P$$!DmfcM6N%RcPXeTQw;b>KVcX$+Mpat5h&q-NtX^f}1T^Yq71J&NEn zN>>KBb8+Di!|54b%zo@-lk)GZQeI*oQZ@An)QB1FI?emym6%%Po__(;584x;7ddeV z%U{NHe){&n`5!K%F9Fz2hi0Dyh*vrUl>?su-5jP!%qw=_-)jXw?k?UFveBH#=)WL6 zKnKAjli+j_4@Vs0ZXhDE+ZQV-@SnYWkmzRNHb8=OYB(UhU|tjb?Mv{?kr$MCrfNvU zm6$soInY!W#;`aYBy|AST`<)j3jc&G1~9(UmQ}*f7a#8``*Ki1lw))NxaNGp@)hGJ zS@K6Of4N@uHvHuT-XdozrL7l{@PVS{3;-| z!LK)kd%eH1;TL6(m&!XtohM@3E1ZXZvCL04Dfg8PV`USY*^Ac3G$=zwFEBlb8W8-P z?Ym+0T@JK_@M|Vey+mtvOf_y;;inY3Of(M-RhsM59El*a==at%%RB5FgU45EO^K_V z-GQMni;aTZj=N_aBq$S};x&olnT_AF)qeGTPlA9MkS5fr?b_~R-Ko>8-RDk%q%%>U z1T7`@_dojEKO-=C!x?>m)IHJERo5m$1zz~aHz_pxug?(xjUacp1Bur*y(u8I?p>*r z1nrZ)FBcX89n?Lz*)v>7H;GM5M0DWE&>Lu9W< z_Vbyl0t#p!_a}ayC$&P43HG^Bm62az`dU=xJSi(fKzn#ZM8sE45A*1s|D3BEbY#r{ zqM{E{88e22|L#I8yhoiqz+r^&5P|vBvf@|TX`b)496kBlufLZofFRYniOh1I973sf zQ1)0E9)6}{(3$S|MV%Z&khWX?{b#KkSVS($W~TcQ;)j&pl*G%>xYpGh@tjBeh=;&m zo!d_(5tY%tMGmn2=NYb@TTn;Uu5Op-`}Y^@=NJ<;#nUCg z%Vxhw@V4Pju*c-1OtzpCjN5hMN#^Erj;jas-)Iq4hFO7oPEAXJT zvZ`QiIHLbicC&wt_)WO$YB0aFAh$2So*(Qvu}HVm620m8dgM^@28LeMyGJ|I1XVy+ z=4Sq=wxz{@Tak*A_de`y@9BZjsjqj@#E)1|vtL;y5+j*m}bRX-layDH2qCIhg?$zZhk$F%jTy0zpcF;*=#&P zUTqpjj<)A6_d=;#2MZW2XYe)zgVUFFR1JDtBMYrw>1I^duE;dIg9)% zHAUG>HHFkmY#W!`2OxixH)Oa@749N9>r4*42Y1QSay>6_$&|tLo0xr$`MTr!>9gyI z_m~s?{>x;L5%hMr_E8-QR^iE#U5t)Bq$aDZO%39YQncf_RWf9|tdQ*1(w^ePCaYp_ zb`Q?R#5(+2(5yvF1}^0%({_(v()cuG_lz!)qBTM%xFz?v3x+bs9hmmH7xB-4Sgp!M zEB+xS<`nf!4Wg~UuV@zT&gs}i>Z0g)S=L%l4#>=UOS-&aWH+iKrF>ZlvlTj)9R9;}7xaP+LErsn9%9r=-gde1sTN7f^t8gcp;UN4rV{BGQ|r z(wyt7xWX%$lBm6@8zzv%dkBlBy`+D=XvC_?Nw`GZ2_6q60*H*pE_gm9#>5~Vnvz>i zPL9?QDDhN1S}m(%)GMIlk?Q#MybAJNr0jOIbQpd=&HTw@W7E0*o_qeZjQp^}-<}{3 z|Es=8yx#&id;AO4O^I|61p1XHCAuC+V2lQOO{jpbyZC_i#&)~fm$ht}49`j^1I05Y z2#x%F*{!^gN9V%s=wB@LU!w%+g-Ib=ca?FOH)_YTR{>unPrt7@<(JJuM3=an{scn- zyBRv!ZwHtiTq3^0;^#8&d=}}cm3_8uniqfPH^cCrHZBuIJ`R;`IkcjqgnBx(I5WmPP&vuz7tXjWSC1vwonT)pQn7Hri`yph*la@Qke1 zlUd!N&TRoYRwamQnV^)F-)IjPn@_AJAIO?OohTVP;vLQ*c-|(*>p8Jkf_}(5aZX)* zR5@n|EJ)?0%&o`@n{#@OpXXh(u|b?J8FpVcj5TNI%Kj@t^YKP>>&nE#>CHl92I}Ve zP1g{8MyjGraNUi3D3lKl(HS%&1H`8?18x$5{VDLnaA1HN4CA8{Tw^a0KfgL@-57pc z!l}X1%aXqYter=d7t_e~(F3J=ySae*9 z7I!H#rN=!7S@1WHzs5g;1Kc?^sv*QLS-7;^k(ojbr)P%?r3R*;LH#*BCmt8!-+$6Q zwC1e3dSTnev-j_u2~M)hGA{LG=uc(h!<)mH-mNYBfdX9l)a}DhT1W7Uo(q(7V~%Bv zLwQO`gc-*un(PL{021#G-Q~lZi$>ff)VKY%-oY-wRlql>yv_+4zP^gHJr)Y1S*=LW z$6_>lS=G8VyS5AGvKaUk7cX(7!|nEzR64zVE-*(#gVnmOQ0kV#*i-9qO>!SghyBfu zJ;*ZUe`hsQ;n$X+&pKkBOf@ZNgoc)O(;hqa|8zS(3PmLQ3RcbNF4afUMMH0ooo+OX zTHNGdesYD5TQ_;&+WBkK0q-MZ`@rut#P>+drLcQ|>vzCV)rXvltD~mu1`(`rilY*d zr(taDeh6}A1^{A-={=t(+kTC{m@HLd(g{gpBFsX82oH+l#O$HWMGqk1f9(1>%RunH zwXR8YQ;$=!4%BU{g~6IfVEh>_#_uW9J>-2lSja_O)U979**5ctk`q+j?hynX)j*c0R zw^TKN1(XCR+iUF_EdA~IbD^Rzoan2rLsLyg227OXbo{cVl;&=@5V}5x3tR_7%uG3& zH!!)v<=CHeyLd^gLlGX+8S`f*APPx0^n@q`i{vxg(oN4J&>t7!L!V;_1|0aXPf+z< zvZv)}1^e4IE39<|XA>z79?I66Y~g|h>YoRBqKi*e1@gx@Hg1HYz(}7^CYF~ zrH9Z~m}l|1erH&s(nNGc*^PDd^y~o`d(9o_)pEw!j{^O0`{4_8U4QLILMi>JC=kD{ zZk{f>x#D`N!}~gu;>S@+IgRX~%M+S{z6ikIElDoDm6z;_w$x?WfHir~Hp$(%5}Kb0 zUYX6Xaz8s@+n_pfv3r$rW=xFXNHBFHF?YPuP+<2UNoCtugz)j4^GA4pq(2iLu+wq{MT)A*BzLheO%1tN(!CsWhv7JoZKc~+1I#W7oQJ1US#mj zos1R`EXOpJ)i+Ina2t;5KJyLVz9)}?`At4XV1Ss`qeyhaEh6FGei+vcWokc}Ibq1C zKrJQc6M;c$G+ME`I%eQfg^>SmpLTl=<(hEA8Knd6E5bSpjGaf){Gpso)}%ZX9I?Gi z>Ua9BrjEbPnCgp#kT(`7=JOP??F#nx;mO;uWbNC8E9YT~H3xzz2|hM{vb&`ki9`}F zJ$v+t^#{3TEza1YJW3VH{n2a3O6IsrNjuGL1|}Pd2ddP{ad5EmX8Ven&Ww|SN=C{S zRThisx87InnWKT$B@I-KIDN>y96=c8KUEt6>#qbWE>tjjVXVt<4}iVvYbTJF{7}xG z(havtb_MGOSK9KRO{Zy4Jm`)uY&^OulMW2K#Tg{{T7N%{ZN0dABj@lrc?c zhQQhkdEZa?{Vv;hygPmFnfW8>*TOeXaIzHml)MvmESRC~DCTPR`=A|(SH-n;HEFCu zg|B~%2vfUEW7Rcy}~vE4YuS8#`&@b8*_K`x0^{>*qZVutc`EX$nyHa9J|w4!zK#%t z4{so`StA%odWmq5q&$gd` zX3hmS7km#1VFfrIBj>#DR&C@UOL@?!-|9|gf;RJS>0AlmFRXLfjxsWQ-8P!xRc1Z| z^5ovr(CS~rO*7aT9kWk#7N{Re(B~uZHYg05S$~%8{!Jy~yu}K?1ngp)%VL*Uq5p!4 zP0H2PRqF`2d1xJ3BMj-}_k5QhEI`6p1CLTqI+>I|+a#coAd-1yvNx*utjf0YxElSf z?GkAqcA;U+$Cc@;hO#A5F_oedZzvEt0eZb5m@&n%5Ptv(EGkW2#+fTJdN0}Z$HR3F zy^Xbw-EztJ>m2;f9S`Uy?Syj))2lLmxwyDkReok(F0&(nUSH!mft5h^R7b=HGZdI^ z0QG?X;^O5TP_9?`Q9-7GUUFpqu=QA}v`)o&2YcAarOg0KOv~Pjl8%dG9 zan0U&xwl%D!UVZ$VV1+>5P}wNd2huxH6M6p^{~=_P}1SEq+anpWUqVU*UNJO=^OiN z`X*$$fbudpRUccsGA(XN`trhiA$_UE(wEbr^0HuN;Gdm842s|Xz(lUHdU-^?1tTBW z+_q!ui2OC+(so=LxgJ{&_z^EJdg0uJ6{HQEcYVBj(b-^onNsVY8MG;~SlX2$fE8_W znYJt?b}`CdR6l4p>MnO^Z$wPYC6|xCNW3+>NEuR0bh(C*6|G?Q6UzJ0C3++b9Egkh zbgWJ;rLE+hNSY1MY0CyGVa%O7u)7%G9UuzI9Fn3RhR$%2F=J!)p>sF_7kU*|vciN2 zINwRPAd$*(vQ5*25^tMh$HO|OSN7<}DA!|-h6`+Mu-K6JZl(*LzsDyoiEbD;*Jlee z6ZoUuZ0gOdi)Ub)t2unUXvlomt=t_XQOJ?YKhRs)^xisUvU`SRClZsTicde=m<+Sj zakU!{7{w+7s|1)E;H@5sM=8XAw78tSyG#LC=P)Q5>O;tzi+?4!*}&SDiR*6a>eqUK zx+D_v5@w?a`=*-L>|RLY{LX-C8DUhZjPbWG=+#7g%a6*M%KsjDhX6cl~ZqFo?{ZjVwE!__eIkj@OJ zfLm$C^Z`N%)s+HPKJ8Iw@tPuKDM)8T;Lxj@Xd}ya_SS$~{f6|H>xGWMEsA?LmY(Szcc#C6$a8faN~n?MjlNFv94Cvyk;q6U9eul zX}l-yOyx}Z4&QsWp91-shU3&+_t_okdxKlR?ORKcId%12^M#A}BYB6ZCXxYo`PXK@ zt993cL81fJb0dWutFDetxY)ur)#wS-fYFUsaH^9+m)k__`LQWM_9NY!r+Z8BQ=rI6 zY7jR?E$_tJ0Z@uv1DSuE()G3inW5}4Lr8%68uHq~vxf_t5|?)rn3=LYE!gO@!^t^u=6Vo#YZuFR~$eMMr8u6a<<1xyf%OaP?)RS9@GB5;fw5}Yw z4*=#DTCd)WOG9{GElt@`s~|#e%3lI1*6Zo*-C!*en?TO3b%)sWBN1?X#RRsdTl{Z; zpIbYpnwfTu&J)#I!40=;3*-D;cH$D(?b7xY>-PtUmR3HM;)31tZlrd>Obm8lH%7J? zv)SF7`o{{EX6zjRZjQQ7g{Kc7%@p`|>|BU!9)(_fCNs4B9dOOw(QLaKipp9((5`Now)MZO1c zk?bILqh*XApasp5s$!j!vfPc?JSJ@|OM zmQox(`rP@m1j)Me83Z(DeN7Q++K?30&LYi>G8Z7DH{S9EuCd4}xW*#$zt}BJQ!QLq z1WOm&Pc|!s$9P)4sqP=ChqEq^bAMZrbxnD1I2Ffj=R zp5P)`0H7PdQS~t~o~vQJnYoRh!sI<&XyBUrA1ZU{yNDp)Z~SGJ$hZ$Y(J>f_*28}|22OT1 zcCr_v5!rkSGV}Qx&`aSiegsRl%AM(gqh3mZ@vAW9Ud5#UVc9yrBl{@VrU`Y;2m8S@ zF{&&?2fxV!xi>l5z4P|PFT7KMb+Ha@Y@*d0TA_hjQ-;0&wYt7ogw3Uw;BqAdFk$yYpQw4m?`G zN-HTO6&(s#{w(J_LVnB4Um4PNJMoz9+f(FhL&!dV!wcd!O7<yxJHkD2lOUka6+*j6d2(Wlm`;#9|)N1l{~zF^{jEH?)R4+J1)_V zvDtITp@-`Bf|_9m+nu~)w%Qgb8Z&{Dy)Z2hzSt>o^d@>U2fm`0boKSn8TK-)m zmA2bQA$@m8;Q(zWZS!-KlC~zpMS17couc~x)2oz}T=?~psBhIgK7->k~a zmCYYbOg*^c3UdfvpYMmRAwpNjBt13K=|ZlKd}x~IZXCzT7F#iz(zL#r9Oa^$ z&m1&?L^E4{wvOg83ssV#K#Tc`O}&`s!)6V)K@5{L!}F=GXKA&SHgVWxk*7WO&w@jf zfH>#yu@k+vzJUslTX z;7@z+pRH^Qd=KP`9CK5bjLx-ziW|ObWR84_8fx%yHEn{6L4#APG1{tYrurOXvQ?gB zg@9dcB!$LdVC5=yJQf6Bk*u3v@jGWI6D>^Iqur`<sQ&rc0H|? z(cg)Wz4xrSd6*uU*M=2SDqTi!yMtjRNKy+p8laus?SCIYKAa5M>uS@i*+_+Pt4Blg zJ2p#1E;ml0NR&z)s3T%J3VU7Z(DAa|&cv}X*H3Kj=wJb^y$`9p5d{n<+9|c&_J5sl zHcbT#kFU}F&hU|4$U56JOX)lNl$MjUdgpcwVo6R?ZCLaP9Aa8N?*lsBwqVMkAMZBn zaJ-j{F({_27*(o-4gD7G1oQL6XeJyx$XeCubMXnWUqhzA6w8&{RlQ@*eR>OX6J$hw z{=B1F--s9zQcQcxFU@OqNw-gbiga6Q$17knws0{oQCFh(aT~V$*JAfB4p8Zlvn^3k zW`kW=?gQB|D7mbBjE1#;B$JANFFXX9w(a)Ovgo;UM?Y%@HMDP>crP~m;}EbOnVao_ z-sD9ulsR*-@w|AQy4Cp!!8wtzU9S%NRAFjEmp+qH z<2s*zY9;S9_*paSn!o=Eu}90!?^S3=z~X(+QxlzNhbH+tur_^mLC2}$bQIX*VRqaRx$NV%hNO?DXxt-eqPmI6ve^cLjOIcJ@l@$vVwVq0(23T;y<0ws0DPz9NV+v8s z570ZW0*h*kF4MS$w6Sfr)c<(j*CcfXNw{mnlkLQ)#rsDYBk*b~9_XZXw9ZLJWp-`E zFKP&WA6T(KEs7p0ht|y6DBx{J;~NzSf!oxsMy31_WR8THCEQ=5XOgZ20&#&&Y0|ac z>>%>O3ubr@!e%urmBfv}nvLuR8%A5;648hS zu@UAisrIu?Dvz8`x4=hWrQvvnR_oF}9{4Zk+e0JJoH}zfQ=XD4-|ZvAZs$hOj)aQ6Zjbn=MM4~9# z1{mH`iyYIVQNr~fs5lJ4`GtV5qEFgY&3^+N1+lHB=AAhzS;z+=cH$I=z@q+!`YBFS z@3GE@b2D_9BPR46hAuCpZDrl_Jz%1EzOu)&waeOuN~U{iJOCG zDSC$MF!-sH1O0B33uAz6b1u$WRJC3AzdUg2Lx)x}Q8vP!;#)WhTC`?>(6DgcKa;44AU?=CUvR&*A!RXLUtoxFh@=y>omuKhAAXFjy@%~Zi&LdV#*kOx+YZABL1SUU3Ut3QL^^mftdB7V?np+& z|7(mn05ZuKb3i-m5=KvD0NR0K+tfTgZt(`P=UPdNZzS zI}N0xt{(`=B(>qOX)L|GJq#JgUsp$*i8BE6!7D|c(8*+2rrtEpOvf+ru6qos-rnv+ zH`kT_g2cAZ=o+MsnWKKZP3$HsD%V~5UM)EQ~IvG>EK;fKNOk~YMv}ZSe|AoIU zs2sW#@OmM<7*OCJ?~`G?{Nrx?`#rRiEiDM0tVX49&B!TG(KZ=JbM6(mkvWE5-m?Kk zE|El#?oPuqlkxVwdodJaxzCJYSoT-^^K@5vMsRoYC;#V8{z&c@0fDr!en`k_6xM^ z&(FbD?fdpX&7PiroA0t6dLLv^{8$i_d-yREx{QG;Cf7L12}bcUjM`Uar!w76h^>j7 z+PYIbOquU%T&r(VWe$#nSuptlEVqP^<32eKsAGK3sJ3tS3(vY`{Rx4=)*~&3s&Xyo zi}%SSeKh57$2Rm7PiB3$opWd*viO;CCGbhbp1~$QF0Ra_sv*zS(6MelZD5)0xYaC( zb0MI}Im`G%J42$>-$JdVVE}en9dj95XfM0I@mUeSOuR$&?oUYAD(y2DQu(>&kY+|C zn3REH{yc=zZDRI|c2bXAkOmsiADBZS?Udo^Y+-A}+Em(+9kyj-K8Xv8$)0cB__fIE z+xRpKh1ouNHraIOjyy1F`{BX~EgPvH{DH;dZM7Y|mdWd`wbE_@Xr>Si( z%~}=vU;lw5PyOx&^lgqkK~Y)X>KzFy_(fzJANnH5*KVW4t|Es+&~HIK1n7B*RgYD<6IYZjljE|m0MpHp-X zlZ;^RcEl74pe@!2G+>@P86|8FIhjQhKto)5(SK`XP74;dODy!OUabYkenE+GMH0&I zoX+~$fF`w&WOyh^$LyEzMUjczA%gGH_=>7(v;VfUvJ-ez6JO!S3&ZOh~!~_Y{#sLiVMauG>ukrC& zHr$J1Zw%0)RrYjs^6Hrd)kY0hk@Ocph*~~XA^uAa^s%JrPu!T;vBZ($ZqTJ`uWHaz zCRpHJe(nOh#!THNe`l^R@GaH6P1nWrjp2|;5rXq1*#5VZ(?7=ko_+I3 z^r_vkxr0u!7v2w7ReNmYk)#P#ykELbsmB=Fch-x}{M0H6b0nMeO$7btq z(28@g^_4f*?TOUG^q6a%yrK`9N>^I*>Qt$g{;@Mn-1_zAfWcj;Z3uwrnCrMk;L%B2Y}!cbda|4323g^{oSWB@q9Q z2OiBfBk=1TWn)iK1lBG^;V8FLu;@Q_e%~?RC8~06_$G9%nuF;@K3!$f#=*HZf;7f< zRRtMBGcP@TT2~6U6|ySLkTqepr*_UoNNKOYo=()CJodAVkxmQaq>tT=8T2B;S>3+p z^m%1LrK-73C^E?Ym9G4L%BglKuR?ir?IMJKkHLLqvy?^cA)m?^nr${ZIyza8~z48W%x^-~ygoYkzeT4P4Q;u@F4sHq0J~|7N=u*aJ#VR{yiWY~g8V^lbr8p9T^La%{I~qhnM1mlju7`9zkuwHwro5IC$p zRqLv|U~Z)q<`i=3*kf%i@X}A)(2c$HA0xHn&YdLX{Y6Q*WNhunxYW&L&%cOI9NF*_ zVG-8iT(2UM%NNU>witI`0UsyRBuvvw3;BJWE z668Z3=*{4_i(daJZoMw{;zJ6EoJQZE_SU{o`y7Nz84Y1*k4 znRB`Qn0Iq+GB;i`({IA+L}Q9#6Uo$g-s4&xvgDf5yE?Qbv^zrwx_V~N{lT3EG|wP&7t#lcloITnL~v> zHNGkGjgTN6t!m`g5a(L5d~?5XP9z20%ud74KhMHENc+dDXRo>WIvDyl)x<}Yxv13i zr? z)Q&rI$k(!XpQ>E3y8E?KOgQwjz2opC%vC?0&@-@Cw3KnKyyMUpZNZLX>Vzqu>Z;$u z1!)Uo3BAU@DWa4OYU#WS>2Nl}JQkKW3gOhNQ2 zX}3UVWlTw=CqX$b_){3^C+lduvp{fW7ap&Lt z9EH8|@1|X4gjpbG#aa(`X`sAr!uiu~OXOn(Dx0Mu9Uc<8euT89O0i#EFO?`zR+e%k zxIf3wPo^DCUaeLv`bPcNm!_LWg58lP_$kEiPRv%2+H6WzE8n9vG}m3>L*&1Z#Hzke zOyZLv$Yt-qlGSMtqQGmxJ6%l)rwsMnN$$*wdTSXI#i_+D{r#`t#F;NfYyb@8SC3Jo zW(KT8l>7b7#?N~(-HmZy6`X3S&6!4uJ$ng5z0I2Bg-X)FSMDV6_1`6YT{`b1nYqb= zm#XT%u)6Sb5Skz!IOpOpb0X{Zs9IQS%Njb?uaI%7ctJnRjy;`ie$4|^DI!(XVK{c> z7f(jvZbl3>sk84$j6OcXsC;o+h5`Kmw9TmMI@9zqW3T(4WSD8a#0Ex3|OaTe0VJ-u3lrvP70LY@~5B&GZl-IOdnvuf^qTAOFM>2X+-x8=g7(sg`LYHlEWONL51ZJNPUjNColR9@I@0R9tv0B znX;xks>!e_(7*3}yB@k7O@N&lL*hZ|StIZat2^Mo-(-f0W>S?0X_eoXm(yIJ2qRDd z3S}hTCMG^VFRBm`Uw3UH;%m&ri`}W!1*^67%0v4Kg*VPo+|&S)CjC@f@jqHQ(Pq+~ z5l+XrV%j#}vJU-uEKR<31H#yPVI1aqVDGrr43#`aK(CeOP@M?at8ZxQwKv&5h1z64 zxy}zgieN97uwE~5lT+tYO+bGAH3Mp!;LF%F(jm)p`MV_78GR1Aix3V7f!{YyDTxv{{OO$-ecabnEpdV>B zf~VjUrR1dt-&UN|{s8L$Fi3+7A8&`O}K)f+P!dma_ z&m|3QpsKVe81A7T>1K%e@}NFRD zC?yrWsgv7cOm0D>l?f_0p}LRb9q|Kou0aS*(KbimTOp!i!m|wGj`w~Zc-c~p>MrJP z1G^^rsOqLL`D~-8cL3swjuLA=toET%B?0M#j+<-6TFCF-h0KC}@Fb*^I{23vM}t+e zr3(>`v_~x(q4r9r=Pg0t@Tdh5xP%xaU65)%x4#ejs~5OvL&PSbn$MkD+fI^}N#^@% zSFScDxv*miKYi)n$e)R4U07a_rxGZ0+n&poKfj1HyP*RCOFpD+FBD%brS*Lf9J}dX zdHv$@$%{1-07MbJMW3_{WJtqOB01;?6L3nm(+K+>w(nAe11tSFarVWxqBqJ`&IGrDY;(B7 zS>14W1d%gCc7wuH$o;`=#1a#&BH`*1b+YZHm$3Ym&h2XaJP%<41}!nAx|mk2cou12 z5k*;`7?+EjmnXyRbbj21pKQw)PQ1AI!*a~)>hRn+HhcMB!|=<=XO!mzzU_GURDC1q zRVcYMf4_9@oLJtkab_9U;y)vdyuvp=N6O+&QZ?OKeD5{6pn&AfaaD`LDc8EmgL?h> z-Yn&1444^T>&@Ejx1?KS{k458lLIC*($%ND>1iFCD+%me{YA~AVhin$VI?%mG#K{I zXv@OLE5qi=KxIaGH9Xf4NXY~$T)Xy_6UBo*3ac#)!i@Y8U z+O*Q4oZ#-#BgNni8(>9r(o|`t^=eOln$fu8Q2` z%)8mQ4C?mC3kuTfpVd_ugIDf%@O(N^^GBr5l}@}^8$4y#K)vQ;rW2Hc=i^znyWFY4 zjlI}KEwXIL%`Ra_HK$;`*@R%F#N)a3*%n2cOX>9X4bLM5*DODESj=~Rh%Woc2$m%P zk^e4p`7Nn^SvTZ4+m^V~=JAgVOEKZ*gNGK1_}QKL#f53;`549t`w+kk)YWA7#AoAN zL=jfW!yaFwvi$f4%wYAeBZ}5VxjrJJoKg|iSqOgN(*OAoDxAhN70yO_TR8mUGe`St z;8jRsan_LdE6R7EysAo^7>|^!Vz693gtBK9IGidJ?#<7)8Lbk~G$3o?+~f<-h7y-b zB-yb4*l@6PMz5j2R+zF~QY*#Bg_{?>q%+)EM!z=Rfm>5`j&wnVLQ?>5Yix~uJH34= zs#I0pip>S7>wZ%(s3*jmg%FG{b4MBh_c^}&wvI6BqLk7#r;?)Hl0s&%W&9N%ca?b# z%ewEu_1#XxT-OuVZFBfMADl5m)jGNEzVV4-Sr+|~e>ZR6ltCqjQXvC^i*=D_^oGU{ zgqyD~iM(+BwG8^8UDcKa^yglPA#Kx9=w1nz4?cyb-%mkZ{}#=+E}VtuGkNlsi#DcnA~kqEN?`8}@=2%=O=3+(&s? zJJ+^xl}CQaXEg5&Iuqw=YSRa+&ZJyFZ9XMVh_O>X-QAC6q!zf6*p^mVYLqn0gRv%o znP_vl{)V$6qxE4F5I1?pa{X;Pw^xtd(9U2UVYJMVe+^e~5eOupT&kt~76ge=C&>#h zxJX>Xxh5Gw^c}y`LK9+JfQVkGv`!aRl~Wk4zXgLLfV+Gdwli-k8bS0TFqmmPA*7K> zzLzl@EhVUfzk)7z>G`*7X?1;rXuSKWzqN)#lqo4hSRPq?w+z44q!;$ti(mzF?r8Da)T_3_11k#ymX6h0T>I2jDplS-5UULcuJN54W4C6} z`#}?-U;c{nYM#sdVzPs@``UTSeQmnQ#F1%RU?0dxcR>pUpy%;m694A9 z!K!Da`RvA|CaP&YFn8OV6oDG|dpL|w2FfLM=iR8aUgvqLtu%F!yb?z%W5{jx^hM6^ zvV_wXa3scU!`5)>U|~hUuB}LWCmHm;1FSpJI!i=SYw^h4{5P%DMN~8WARpPhqI=T5 zA!uUXkp4jxm>oguqqtT`Zl9*WM;Q_3wOP>O-|S#;VWu?P5|)>u*Llronbi?mx1Oe7 zCpB~x{qSL|(A#%$V3^DQ;Dm&JJzj<6U$U$RFk0qvsirU1W(cOvQA#V+rzvzGUTmYd z1C(*rPsC*>=g#_^v8aUe=Yd9)ju~e?ybP?J)8clkJ^6hAKcq;1@6*X*%t|tELXMmu znl|5}b+O^=3$Wsj;y%7QpG~9|Q7m&k6F#v=VbRvMz!m>O5xO(S>u?OqqVP$`DP#wT ziKs(CBft%ENAngxK>$2Ja{1_29vch!I^hRpsq|MKFiAfE$0ow_7vXGtP8r4tSn7Dy zrZk<$bb1rcb``uVx)tB5KmXkR>x$JsEC^Qp^SAB6|M+?N;r-tYzsfp!TuZaGDn>Hm z%{85M##%aGi}n5IKY!fbdkN~+tN-|B-L_k5zy042pK^Ws`)8TkcXM_uZ?)62%&%kv98avVr*vPZ2I$Nii`n^5OvP)=#iAKnMd-?+x+MXndZ9P&sT)#4LS4RtcWZ-`3 zFESgho2FY}McL9_%0SS>-O6j_w<4LFVJ8sl6a&wFV#vr! z9X`cMdsj@@3S%PG@xRZR(3LX*@zrIo4%k)wghV#fTM(__^OHF{i1qE3S`yM0Wov!R z=6GdqZ=_+>hQ5RXcarU*Mf5HX##m~`bMpGL$cb2s?0!-8Ev6Fr+ z?U>Ev-?;9%EUUNRd z#m_TGnwebOfMi^-0-}Ot2aT~ht1C71zjq=18O*-ln3^3QMP-?Uy+lP6cRG*mNEiURoK*&dSNSqHS4r*=E^+ZN;` zJ}fGnXDFq!L|j~4%bI_^CQqh5dVbA2s#Aa>5$D+HT0W!$72|3UpXEKpeQ55#CX&z- zjB?NRez2&?5b>^XXv1;z=jr!5XluOLr${2-3$|&`Tb7w*@J6Shp8W!wTviZ+of%($ zJJKy?uF_r8^lE7Tms)cN%ZJ`g^W=$jjZs#AU;rT-cfp`%J{*l#*j_$oiVUiVgogY3 zebmJ-9CTZ{^?C6sM(ZmC!JI05skY-20fFhP(EuSflS>EoJm%N`B|GCv%I;I>_u4EvDg4I63y;@e%JY}XFhdye0{qnNb zlN4GtdGg6GgOx`aW~Hut7#rV|TT@Dkpo^H!=i$J0^my`4rM$>Dc1%TBC) za6rSo8K{oHqj!CHL9pswr)5Nk8$s|*6@3){a@nO0L(LXttm-?oqA&jkBqdfpCE>2d z1xiW94NVyOnqlGaga#EHM*cE>>1wse_-U)2iDxk3gpWp8<26GrY@deH_dx-pEhb_g z8PlZZQe#6!XG&8D4EwZqgy=?k7d=!Uq+>va-X&tN-UX>4{KYaxYreVD#fN1*; ze3G7<;JJ6=3BGfJsvhR)Lsp{BKU5ki z9j=KV!B!eB$8i@-JMNBNWi^G5wK>QibCa*XE;uuXZ-z0@IwZ3m=}w!0vbWjtRB{tG z2ql+8il!EPQuM-r?8DL9cC5R5pg=&PN(M4YWDWCu7vJ6yj7r}ZWSmcQluaBt((tJT6z3LnP{jqvsVcf)X&9#;#xh@;4lMyQ?q@Is|AQa=KR~%qEFzRWM1FhJ4pNSsMGxvx(5ORmM_ApvW9ds-lblT>-tII|oTndcs&KKNfYZG($9^^(y zL?*0axfBk!OGIPk_ZX>pEFq0xK!4-zz+XO3TOzG+x3ZTWYR1yzxmQvIu{!^GD~^sF zsid&t2AE@r+P5VbKH1G}k%!_sj4@CfCgw5RVrWnV-vEB2CiHB@Y`;mQyvg|%W)~hs}Swf zafx2)t^R&!Iq9IIvgO7e`Xwz(NT3}@IqVxXTv?XO&_4AHSLVR$v>6{=a*?n7&wx>tvxEV?|w9(R538?ew{m8QyG`@Xul z#+Qvkb0TuSMsCSv!)ZQy_2@svQmpKxhwf3%7)W;bq1Uh?-z>5D5%Kgoor0AB;`WGY*CWvl$p@YQ2tQk0kW4TfYF!HtS z4zw(LX2L%!m#;*N`^jt=QL=eO!NIb6J>>-?tdIWmCC`baTsT~5XS~+0v#Kii{KR{K zKlo?k&ALk7Fz834O}F$&@MIpPd|F>V^larc18s+Hj|a?3Ph=q9Y^=xYku@nJR&G+1 z*?PvC8!L92ejjyubZDUO_~nySo!{ua?ke10!sPis*y_c@N$kAqH$XuD`$8jhkZ}$T z8_S1c=B=o^jUs=glO1h3@n1R@lgDj)?!(i8_U?Sin87fuX!4xa(MGvzAL~MJ_xV zD^4=eqqA9Ue!Ut?E;0CcNr%gwcztEd=f^=iIGu)iEcZ>7kMF|59@k^QJxQ z?f+C^Pik$0CT7{0LCR9POTCepJUH{wvcZz{j1-}`@f)+Zz6v7UQjWqLXV zjd<=ramWr1|LlH|_~lJML>69CdK!PEa9d=6B{Bjx_-NCz*75=mEo>dm7oS0iyFH7lzjjL$|?roja694ja=BQ~U zSC4mS>~O1^yXAqvJ8mhK@(Ik9YnR(oi%Kt zUTo?z(zc&Y@qq>;$?=VN(-4>RP>U(b1?Asy#hDUKI)mBStjIcH#yN7kwd7i2Yn)^nS1V`JA9>8wM~*BMcQU>@KMdNs;X7{t>75-q)+bpT zIZ#Q+Ufoz0n_+f%t$9-Zp=$j6?2SI${-`57Leq)x&na ze{@@*hqd|9v)bN#okMSEkf`XJ*jiPDT(zJXHsqwoQjE+oOWg6Fx1GG^GNhDwDs=ri z4%3D6`&Z`~bJoOcEmlN$P-k>05KxzjH#aPX6#MBI-CmbI=A>1A7=hlXoN(utCpw3# zMRrQ3Y|>_UEB3oV&8gam9XYLmJqeQ|pVF3g&(P{lkNrMD{VeIknvn+jCAU5xx6vxW zUH1c+$5A;j3cyVf>|jHla|_iCs>8I1qnC-;oUNR&yL}~br*P%e2TpNBzuf3IfNSJ4 z2`D#?Q=MK1mmiWQvfpQ;VLm+8X3cWl%_+@_OH5j^V<7H=CAwx`Ah(2-O~YEYYduYv zAly-Z`HLh9tf3CFO_jaj<{7)OyNh*dgpX<^Snc!dTj+m5{YR-KvhDfbO1r7IrD9?w zqy4tTM1gaBM%>u*2C#DaJ>M~#{5b!&+k@tPtJi(srX_XwxoKHKCvj#fAPrZmuKu8`w5LI#mXYmUm1sij(YK;cnVIw#7%P$L*YXu(jsa7bSGe3mhvd*-gq^B9tBczfZ zhvuC?{({sp$SBw$WANP6xpf*amU_(8^5M5zt5%v{SHl|Ym_^3IU`qWi%2tcrq?yv~ zSD0jDQ6gM$f8%Q14!BIw=90a!Q)}6mJCw5oiUrWfHLfNRx)T^1jJIc-rnaPV#iMOm${s$-qs?Jr zR`!U7t{)UY;NCn;nMBu^d2ymmIk)D1-oW6Kkj$s@ZEs?adp0Uc>z=FqiyJ;cfz!M5NATrtEuRbZ43qXbmkjlC*a*H~8)FNDx-6O z%k8&lHrDgGphD_UGm@g{FSyi)%8AK>`kO0s2>Nmu95f>h*BTx-o_BL6z?VJu7g$Os z{QFG$WsFD@|6KZcEE)Rr${g@$+Xj&~ZmPsfX?MRdf3?6UE(hrGCrNv#W->2Ws|WA@ zO#pkma;5B<-31DMQRq8DmO5;=EiHPy&Ds)He@}xvjbV?;W9tT_s^8wR%NIJF%i`@7 zhyVw3!K*}U>$%?v&pNo*l%>9Ub)L9wpNPPjMRi77*1$ zGNO`i$(40RBzF-DtMUF+$>=MQjc5506{KRI6$(>@IElz06{0bi;4Q-h%OYVKV>!XFUH!ZQ~O{zY0#*$Qn zId-EQ`U8I<8CO_fg2CInXmL%I&JKE$03%7>XH9lV0YgcfDSPgB(v`O;m>r81zWyJI ztuSQ>>(ffIG9|g+V^i2OgY|l;3TD#H;hnq-byru%_SVF`toz)byqbIV({1#o_C>#0 z)Wh+g4!B&p$kF0IJD|(2X-_&GfyO7ezFRx%uPp@V+X1P=jYi*FG-&hl;b#Vq_PBdQL8bsJzyR*jbf)9x^8ub z-P!|S*=w2(O`UAh9v=?P7E7RD7K|yxt)^>^e11{zPJ`^5D=CPzyvsa#u9fF{t?#{k z&+RA?CY$@kAS}`fdzyYx>LhOUrC-QnY#p(fuirF~2Tf{!o{x99@`<|FcjXyQ#a7i@ zX|TFz7`gi0U<@k2UE_2LE^JHR{D3v#{I~gC8w3>h4iVK1PIrM-g7ZUTq-n{R_qCLD zGzE<>Yz8nXhR=O6%_0R+Agz>Bd=&C=e<4ll9C7)0@+l6>WUgZgzo(FBge^y`PGTTe1Br&aE&KNd8F24G~dtaT7vhn z?~``j_oDUnvrn}JF8;iX$Ch9>KrvU`a2#9bme>L%I;2{mMeRL%p`O9vQk&GiryAe7 zpnzod?mW1B_4G!1s<$WLE&C0S!D`4o*7LOI+iauu$W+$gmp5*lI%4er>IB)PQq&3k ztc31-c>^S$(YjWbr{DVbhKZKN%Wzx{AXu#IWYORBA~)0Le>89rC!vaADv*7Q?vT6r z>(?k@G-O^hw^DUuu@0K_UwA{kI6ghaM5N)US7i6)_Y@Lp5e;_|83lg$OEj{p7MD)B za7`vhr!!63=>`gWWXg%Y3=#FlXUEHBw=bTiiBl)9WjOa#PlH0&n-x)D%+&i4O#P(? zqJkud2>c0UKnexffisqm2Rn1`QH%Qv>8Mllhm-49()UTT)mkU~Bxc!U*+Ig=3?CGSF z?OhwSOrTCTMY0$jv}||{aW+t}cCQ0P%C z+vYi2yL0J}U*)f)Cc{`TcOYp*4AGt-bwT}%N{&QC!YZlZEJm)}gZ)=E;iV$ec3q{* z)cGc*r2srY#llXYcwNYTiscaZRbUh{2)U}qJ>Yk<$g~`TS?8?mUw`B~IftGg9CSaO z+>!iQg!zVk4>lMVOj@Y?{5J(3jK+WSE|t{$!8nGPq*=;lmJ0xsjGWZo@W%skd=O^l z*Le17bGb;pRwlbVg+3TA0CuD=6CbA8k3IXT4V}J`R?7L)>ue^#J-F`_fJ~|_@wRwP zZ97CWJNYnGg?1=&amrR|senu~6P7a5%Ca@4G}A>`YzN1E>DHe-@Ie&WspNfz-}=sm z{Jg}62FNhV9d65^U-UCeQ$A#hkM#Nx>J*`tNb*G&B!>2mZ~<18NT9k}bp7#l;pP*+ z(1;W7cHp5e*gR`y-Dm5R(BcMcd_5iYxLx#CPDPdaMjb?S7N}A-ZBxH_@v+5?Vb=KK z=5$C#cg<*k%yG-8QFwxH5t+$28saCWo?HH@I!(Qy=aqRdHC z*Y+fC2?iK>D-()6P`%gkrPFXmnv1ZOoNu!J&n??d;>e%4bfNGEqmwqNGw)5DcUFIX z0uhM`Pu*xUn`W#U=IeF*K|3lQd?2+EE~K+p`S{!XD`GTl7T98ly1u)?^r*l~|5~T# zz4nLq2X+`bSX-|Z=l^`62ctU33(G7D7+v=T=xbCT1Nyd7NHYSz`Mam()MfJ}&$?8OxmYfM;WP&CWkd82x(yJSV^fz%721*$A3E6{ z7LtPb%yH5BqX*BJ!+~NrvdQ1!Co;cicy-hU=9RbC-(vsO=;uxAeJjb8T8{Oa84sX_3RxxZ84RO?g__o~$)DEhVdzSO{IK7w33OgPjt7m_5Mu#P;wY zsrho)#srq<$_4Jf`X22ufbT9E9AmVvbrKzH!#_6fw0Xze|K#81LM8yFdO|vVEc3HV z+M7OKOXzOQdc5`k4bO45`Y3h(Ax<=={I&1744z(4e~mKP3kdWvcze)!+KHrm?qlt2 z#(OL!K>mIa5LoUO0Qs4ML!%#vF`XlgSx75-aFU_&fATvBHZv*sYV2#YYA>CjgB|De z_V;Hk)I7HDO!u?&Ij%AWnHXLFDFv!s5My*GOsrv~CnSocG%WC;Kb<XX4Cow-jNWic zAt-aM{tdCt@0!DE$P_bo+iYCoRl@oF`5KataG3olrPTcQk>QV*wB!`p5gIYNM`r3|GJ!=PoPRB0&`U$uD=7BFS&`j)RHGMbGKZ^zWmijB z?ozPN_Z8@1?j<$lX;bDkr#=@?VLw(BRFMpwfvXz7bFAk`EF)N_+8*D#rZZ-*nmVZs zqG9KtVTVK6{Y`6aFPTPz@_7X5jZgA|kdvzBCc5uTbd?U&u~9vS$~lS0-$JC}<`!4Z zzs;==QYN3;4<}CT*n{xFx~pt$rclTNDj0pLz{5aksE~Gu8Oe=dQL94i;j&GtAAkX0 zMk=p;qNx_z0l%K^!@Wc+)jmlyQ=!p*p)@0s$8ASjKrL*V-$$D{zx;bD--)_bVy?Oj z4qW4dS!_aIg|R`pi*RtSn#a<`>dS0@2lDpuA{Is*2st?}^AMlZ(3L~SV~l1^EoWbl zW>GANuIL5Wo>~VdYp5*XUkEyY!|ETorW4(lvv{V?rdAUq%*6MNwo5}vz|j?%56O$b zQ}#=03Xw&(NXlNe{1jNC?|I+D%XK$zX?TopY*Zw!jcRONsVy-xXd0ZEjkg;om*yC? z^Ef9=H)Wd{Fjg89z{JqAzb`WbnWrVf;^{6KT5}by(_onGNk##aCz7XRm-6H7dSU1F z7U>ue`Y|7s52a_hyWSF6cO^o{Dx21rR_r6qJYH|9I4CEh?6Pkb*#RT%eC6r%g0@-^ zVn8JIgluaIO*Y-M2FNbR=WjK5KVRcDJ|uSbZO|aLe2j^7U%_pydER^c*%_}6`@aRr zy^}BenC++ANHE-c)Eq2+8RUW#*|nO%j8F=!zco7stj(i;ylul4w8hAD_Ju|!>0CWU4%47 zg_#l8wU%VU6*TdKiwk%kl_=n^=WCdRGL;h?+0HWpdD#FS@Akq9vu(JYj=XJ`w=n&d z|Dk`G$2u%#E|0sdh;8}-jl9I#3~0o5In~l3OJ}P?q^(IZIv3wO(rqyM^iXiG)e^;R z9yYw;oixfzhauXOil@ZRfhc<9-6tgiytr(P!0vbkcf@4$Mz5W(L-xvjVoD0Q=G^~w z&D7{k6e}hv6a~Ns!*vUj#|@Tcl_w`y62_30+)}QG_3N)fd~XW8->0X(-+T%{9&uUB z^-+``3UK&|<&s`An7?JfoF_}K-+o~8QDr_afP4N)-U_9Ti)E*Kn&_YP$W?sSv6R6i zh7SSzt#`|uhT$74EHP??PQ0U5rvK*76TheUqi|bM9yL=NGqbA13sG2>)cYl}vVS%N zs2T-xNox6T;=`0r(BVaSX269mJMnlImctf}0fLWrGR`u-L3k4c@#bAdS2n;YE)-d* z2qegYX$5UFx?urPhk@Mke6*j*v+q$JMBOB-Y(jdVcP1ksxiP}gF_HsoBbsml@}iMr zj#>NNJpH;>v$g#QMM@>uV7V_^It3QtmVpi^BERjegddfE{*n z4uyW$!X04mJ-?K8)DH4}qJ>^0^i2wS5q*7srNT&!6jr21vqmxe)*&cHn~8R^1v=uYzA8ET zE&&MB`#pA%Nm5wZL>4#Ti;|#7;{0o8Wb5?kjkRYcr8)#w6!feJe_|xd7J*DK3_48{ zG0>QoNe{zox99{@_~Nl?KxOjY?;770ZaiJ|H%5_4cL3jlsA_3`s>QpX3y;J}aa=SV zc*8Oc4=xGJVAkObL9*_KHxya*$w82DxTT;3TnR|26$Vy19J_TR?6}Ih%M9q+oW~Y; z?%+^HK;6B=RLQ=r^6vSbLV7noU<@g;ZP16dKut>5&S^7CP!{3e8leXY%&q;E@$DhX zc_k7u7AJC3>Tf=7O}2z4>C7|*eJwd)#jDkH1^0$0kpmzxMq4H9fGhJ>kY~SBxA*B0 zN8}00?c%}O|B~)-1#OrRa{1%GFA}_neZNI#m{#GMT7zGFETP9)L>Bk7E>-jz9=z0@ zb;m?gN_Dl5Mnd(L<qfk&tN;_0d#RkI^Jq#5@5QsJ@r8rXl=<8b<C;hm#vNAyy>Stc4Q=uUyJnJE&Dn#H^H?9&BmGCT&ucKRB*LD{dQVkYg7j zP$j$+`zs4<)9;k>%o+Xh> zN6;Ui9-Z8zN027wMS9k5=a;Pw8jNji+y=SmUh4|Sew$B30ZoK)6K}Ee!!R5)KvDlL zw&}_yAk(@S+W}V1vK6cM(0STv4S*G;DOmAw$r%|8q5{j}2N%XLynFLGuq22*&cvJ& z#2c-N?tSDVq4}HkbzdQOqAk(bC z?lAp)FGj2Wp{fhP;PV|F$Kf^m(j+>(){#s0QAWW@O=yF&#im=AKiEv<_Sj7xvB+5u zJg>dY!&~S4zJpIcn20_2FwN$ea_Biq?m-t^G&5l}c*`2e!(PcRH)<2KEhVL|Lb9)h z!Q1cC+Tb%wVySzUL_uQhCoD1ykc%Pk>c7c}rQd-uOqr4;T+e)()OKyB9&hN%_uTa? z-}&&Zzk9CIg4Wt0(CR4b)?DSKa!z&ggJnGx8hu4&&m5#dt3v+%NlnZb)Y@!H7rTv2 zIM?hMJwYa;eyev~N%(frFQr%Jq14G!ZH*vB9i`6c6u;ETnv)(D>FWD^R{PtWz-)=D z{k_&$Ps|f)3N-HUn12nfHJzfa|M*Pnq<4PN-ps7s4)I_8I;MU`VINmNcPnQ-H1xYo zLy||nfG$6lG6vG4kS$(hMeMudxMZ^rW}W;WL(>i=W8YcGcth z>#@9F)izq_`P}TT4YIepUG)UM`8AV?K=0b#!h4P=F4H_F6mdYT0Q?b2p#`QB+LCy$ z6u$OncFjdig9SW%?S#gUxtxBm+nho#8?D}!u=>8cT33GjB=VPjhqhDB`c7B5RBZ9R zkDdLw80#Vqx$y6uAQkw)XZt!kBrhJ#g(Lx<(|df&x)^)H+dR;DER;O#RT6qQ)Z*1Q ztG!B$-%NHt60Jg3e?!F#Ph29X+QM>y z)L{9Zq1w4Y73BheGUm?HhnRzDvjWSFyh?n|mL0c9GuAyI@VqJ6YG|cuWJ~kf;EmjO zDmw)XbT5?>KyPHLWgyT`0_b6JMBdDj%U18FS}HhDG^J2n{XzVRM7)fXplmVPS$w*! z2~-;{^agNQfZAKCM0v<)L_gClOzdgQ(UH*^bLpV!_Fp)YjCx*NAqO#8Z_M0qzLJzK zKg7XJh@Ye2O+e?Wj|7h^6U0q3Oye>XYld~7BUV$L$}j(nPX3(4D?6%#Oqj> zmo4!ri2$6k5-z1~%JtXj@B6^5k`zI8g3&yxS)z6bt?ji?*b!NE;F{W+@Vu?H-|Af< z^lw_3Q-J^Vi~E-&5`SDa@<}>}+6sdM75N4K27B9?VdT7D0lrz@ovL)d`wmP zwX46cg8u>STuW=f!G4>72JpJx?{h#yemxOs(a*J5kEAALt>JenB*Stl&ok{~&oE}f z8i$WF%7;)QWels6%PvHwU{`824b2mSTYxTA-cxiv31=r=Pa6V5VwjC%RlaRYoGt5P z33Lu^gVp`0?PmvSJL@7HR??1gA@2UE-#Wncx^(fhK9Xv&DEafDs8h{FCSrZtI$v+$ zS_b6snJ_~Bx?_-Qp2s=fO7GXtIZ(Wn%9tEU?nv&Y_cPB-D^+>fz~T09BkXp;I+t^e zW#$?{0{?_J!T|Fw!k4tM~PzezVa!g)W(M+j@=uhx7wWHa{&;5ao%3e zA+%SQ;drbQ%9g5U@8QK$v0IK#%1zUodZtkolseR6F&IiQINI#FnmV^l(}_O<5yHXs zhStgA;>k{|EwFsv{TVZ&un5{|y2qZQZBXpBmVUOC+2L%vMfGv%R#ltTT4P{wL_1tI z#1q%Wh5~=_Q4OVgrs>_)y%Adm+|8jbefVIizLqQ*x{<)E`!=9}B0>xpLsaYvD^*CZ zuCDekd(MzJr^Rau>bxY9x`E>YMviYUStjv+WJmF_Mm~;&|Np-a6ExLxs}SpRthB=q zZ3CY*qoIV0)uuSNo9xch+Sjy=mLj`D2U;Or34)i_QS&n79DiHgpGpViW}AhdrHtWi z4USn`69k{mg3#}1s(JPUS-^YpIz7=e5F&1FmJ&QFR8*(=&iPM0E-uasTtDgHbiJee zo|R?J-0u@kf$T`3SOXX5wgDf6kw)BDL0_wUd=k?jxEbQ3I=y^4yWnniijk zQ-&VQ0m$=iaZ3BDoQCaaKN??-+xME6ea9U9IBVP{3#3L}nmEVV6s?@bnDyS|b<#}Y z@1%*4d4fx_T%yotuXaR$7IDe?%H#(iD<9?*Pg;ZW@jIrx=xzdj%zpGfK!H)W28vsonWuV*d6G_blElHl#N(Y445mCU@;c!~=lpV%{l{4u~q}5n8uQ(;t$g zP5T^e8bIM3PEjY4jAFA0vl%DT-(v#$nA&tYhy7V=rK);Z;%MJAwQaD9k-d_XhUnhV zn4`p!qwGR^3VdQ7KAKA74j@gFOj(Va(~n}v&>*gZV?A7lH>|AlkAN!xU2n{iEDYzs zXWn(|@fIR|Jr1O9Wz(a{^~QR1E{LI z=BC7{iNfIg@U70KOI2jmKztdB>oEE^OoZ!$$z3B&yV?;MzI3rNnuGhR} znR^C|=GCCj?v~WMnw_=u)`c0fH&X+PV4fAw2@O-CI2*&HxMaUdT;cco;6@PLUR$uN zc6pb!*;Q3Ms}kXKC|#Ke&5!tP%txL<`yO^r1jnAoh%jz=JM-dqKV8gS{@ts<|EyY2 z?)?wZ%76YDFa6+OXH1QIU&MThf4=50XMaId$+C?9&)0j$MRj%G!;{A+@t0VLA+~^! zXcQF%2?|Kh7fVD1mEKiUM1c`0Lm!L=QJM`=se*`fDbit3nt*iaFjQ$nFGHJQ-hD1Q zb7#!+`cKS99p>J1&OUpuz4ls>e~WItvGd9ucZ=lc3;)S@{q2Pvx4!@F$DjV{d)6^V zjvxz{sPIe8FXc5(x=<(jXz^>?77ROt3F9Am={5w`NjbN+|Gw|31=wgbdTzyjvIS_^ zSu-5_535JuCmP46{V!s}yJlCI%-Iqjc*~I$^WajsmD1O5#dYhN99qjV?jZI zzbyd4qJ!fmNQvA5-10g=Nm)NebP^~iIbCa(!>v=<~05O}EFfce+ij!h- zk9x_=q-y~P`7`2lnM_$=^qV$s%)1w_bS}{He9eRH5{`=_T$kt5N zVgr~w8=x#1M94k?7xlSL~NP4osWXYGbSS-UHOF5_?gXIk1bLE%L} z*aeb^Z=pU;>RDxs@7EL5)fbJp^;}b*qQ^(|xI6t#ABp0%J<;*xqn;j^YM6goRX7Y> zY}~anSNFNaXVyJbr{;CuA}*Oax@zMQ)^E2pxpqL^2&v{OSfg#~NZsWaMeK0p9-wfs z0%Wu#r;kEFE*|I;Rdg&VywvGPPWa-s`rWVR zEiCr%8hk&3h*MJrm{8^W_kV2V+xY+u-Zk5R<@r6b*y4UecyC}#W zMYD#OV}7qXiBo)Zx4OXd#Z+efsDHUiPPCkC3#VVUCeGGGo0G((t#&W5;uAlPZ@z! zcmlwV=@U=x;U9V!I-0SY^FaO`=90>s6!>=(J~sk8`ZSU??UVA4)EEyi4=Jh$H_iEKHIr{j(=UoyXT>;xq0*EV^pw8 z^up9-ZL^d2^gzEpIyDm%mE_ zkWrvJtMMd+hKX1*v(bY2JI&baX05FE~FXwskE0Xu#g%)B%`R<8&_IlU`}tfY9PN*d^T7iG$H%7;OCAmw_5e zH$d|ZXg?VA`{hR}E4D-W*yDe3GhB70au~Vc5D4sTv2HXXX|>4rJuwZ}j@4?hIBG(* zOy9XX5p$}K4Hp!K+~XfW_=f3|0cIRwT|$*2;6itkcXpS}<|fwmeFMF|6N9u^ zu4PZ$Qw}`pTrZ(5zr?(dw4vsx;zc%<54cT;vwo5WPFM57!?aeh4KU2dpWZIj`dueQ zKtSL)@7iPK;t&LR$v^%$bIk1Q*{}y{(gMHyau)^&v>IX?Y1Yuy)oHk@!|y9wrqa!ZUL8+?1Wv$;-@hh}OS zgHTYcPD@RLXfK>IYM0a}O@X|l6OiS%8}m9ljL;9m?{SJbp0>?1{MtMB>OCtUr^Kop|LID$W1OP(1^^OnHVqN(`g8l0aie$ z5+&JKE{lA5r`z?(QV9A@mjWH?k}q@$V^gzCW<=z1WL{^}7i@|d$}k-FQ1 zcuGY=cJaG0vXYaC8UU4Fz6;?paWvfV4;3&?N$;L-N1X_s#I_z*UCCJQvBV#LJjlVC z12q-6lyT+Ns1xiBR1djcV%Z9@=q0T0&I~B4&@<2U+L4~LMxeU|XqUBp=K$)+2`aeC0qOh_%IdeXl$PFpjMnJsyvdgk*XWzK-`W1>^=%4@ z_ZykRW)hAIFyF~uH-ZD5H{^AyV}@$BvCd)?`NvE3CZ7aO#TF%XEND-Uq^RRaoXXcP z>LE4lzB`PesZhTeQ624uRyQzMbObuiMI3*rme!CxaDcLC|xR< zzRsW%*6J|=Z9d<{N>Vr#D9$U!3$eYA=q&7H^qx+79?Zva3On0~m5{h20teSMw;Szo-%7J@2Sf?_)oo@%{L)zH@)Dbq$Pk98Lj#y?Qnm{KZjk^Qfufc|6G?uDbEBeaIckFhb32-IO+@))t=d5W!8opD z_62M#CdG8V+02Zl9wfCF#d#F(Axf_{1NZDH&z3K^t; z@?}3|@T7IVb4MVq|Djo8wEtlH+|w#eC+Sx5``;w`GkSL8n5fI|2R6Nv?d6@0rbD1F z@&aUvxTXpAk1jxmcM@(03#Puhx*(bADB)VA0MS0=8N!CRE4dtuB2Xk9A#H|La>x!A z%>vi?96562JWPuDOKsev+sUfhn=u39EY1B&p>_rKRjyWovm->jwx+Uv?pI|jdbci& zZ?cX{C<-Y#fX3nt17bzmb5^xVPZ9WswYnH`^0v9U46hvlYhrau#1*HW!hQpRi_r_5 zNGN+MoqE_$%;0g31l;YM@iqdX^J#Ocxkj54EnY%iUT_vPxsdX|K~5soZE-}@cxCF} zpb{@ZgXu!!pPQxP6o+5W^>!Dn^V$f)D)I;Tzz=fr2*Qimhj((QK{`S>5!j$GiN9Ri z9g*H${>Ni6BYkrlNd9~j=uHu|m$G$Trz)%`j)`1suYMwXfO>tZ0w4SD8594tvb*x8 zcTJcbV;FQ0!9%3mf*CD0Cb&vG<5!;8|4RFFXCR0MmgY^VRz??do5jhF@@7j6{N=A1 zWdI59ZI1`iP-fwaTKA}aNYQ*?oLwC3lmvl^SE{h`@@b8&-NdcoA=U*4)>*$@uX70I zy^Gf?iw=&AR4B0D_g}B}l&i|d&p`t0V7qY<_-syC2ZwOXg4 zPcXAF=U18sY`B~eUpbH@7NOzV6SY&?JP$Gtrm9NCWOhh&lyig@X7z^x0+96?87F( z6P(2a-7eVno1wg8Z7>UUwck$qsJs|+wD8zEiETX|ty{a_dAo)k0H#YDTNlgyR!oLX z0&;P$^62`3de7Grr`Eix@2;Xu_zVaLen@Ap0pa~S2nP%XSSs|H`e`*d zQ(@8-Rqt_}t5*392E_o8wdVPARoT&NoPQm5z}wfiI2v4Gq}#5kjP6nvkoj$^eN~gN zkEhMnt<2+d%GoeNm?^^ky5Fxqyg+-a?JVwe91cHlA5K2|xh@KX{syH~R8*`X?bE>} zz9Jo9FY1u`jwLSD{|}5e4$G6>@MVu?l=Lk7qtS2N)pQVahK*!T>N__YS$BbdPEix+ z4;=-PV$$Nfe{g+JWvopHx1e8qF2L4Hk>Zeb13CezfF8c&9kS~&njWZzGd%XSMdzDXc|&3h+u>;j0%=v{DTj&)rl@ zGjGq*bzw|Q3=GvbOUbhe`Frva0K-|?NnkeCCmihY205%Ur<-FAVAw7hm5j?K>uJTX z>=(-)NT+p}g}#~vw;tRJH_(23(PbRQFM+kv8}gsj8wy*y=e(I)w^sUi)_4oJH#C3~ zlQSErzjsPHZ)5I3=+|q<>-&Gs|15F9E(ks?1G`3y1%M$me_V&YI7Zf}}a!You(_tbYueaj>`(IQSVWocTd*QgenTCv+ zAe-H^8MP2Wl>Flp-}ey$p99~e%D@18>L;H>bejsCr~}ve+0|idrVqn$uX$))aCqHB zHXQdEo<+y*z&Equ(*4jNi0vhNEf>1U=L$T)FEw_zrI3BoIf;v9k5mKWM{k(0WZmX~ zAK6~r+(s|q*P(W zV?x^i%wWl{A*OZapLW!tpCG;&Q#!66O3jc#1Hex>SU(xCzREQ#bfKpJvU2s=9#U ze5nPKx=Z6ccu^N=%Z@Ha& zVheAf(GT%(``9$d>EKu0q8T)MdX^T)s^HPBXFO$gZC&`oDnntb*n4>Nqdbp}!39oV zVqF^5iS`6g>q)hAhw-;7dlqPkiK&htgpnER9FzhAs^{wf!8Jxq%m(=OxQU#9dvJ`S zf@#gv|AGL!UIhAj1cchCeP|~6i%lJ&*h zQdifBzh`r}f?9B*|L8J&_av)M4V6`R*!FBWAov}9A%xTnm95>cy_rY1b&IY``mN`? zuv|ds;Saw2`dt%UHln!@(@-C@K2}F$3=>1K^@9DLG2a&`YAL4sI_wkcajfmUbB92I zjxl)T1pTwB2kbKN%uRQO;XFG`qL2Sw8^SoIi+IZCq@VmxUVci7LR+>}q;+K`l3fKE zH~65!{PH~r@lg=_0K;jI-&39u(=E_&6KD=k_Ss#DJjwn$gvr4)>;~`k$#b0I5E0?h z+-rcw3XHO)AdvktBD8LmB;Ct%?#Q9GQ0o=dZvV2#eMlw$L1liv^7deK;~HKs-R>_% zYPV}_a-D4#wMG?y+G7V>jq!=uGoYah$Apa&_Q98<5Sk&MoYk}a%@_^3SQt0 z`_TD6)$cAH`W&+Pwsxm)UQ~7lt{yH`u}tuX$Q9Jp)uk#6z^uQmY&+QK35jaQy-Icb zAW?ECUOP6mIi`UzN}9L{r5eoxVo!H(N0v7yt?ZQ4FJ8`;!(6)A$!6oH-GEi#oEPEXH*XLI3VD<83 z;0BGCy*WEZRs#_if}W`5XiAkLs5hCIr)c>QTYhFq$ZR_OxLN8WGz~vN3oRrh1b4|0 zF7J(ZO~U2p1VZ&UWj`otp2E6rsaCKG8B0CFRSjdo@&`8nA>2X%d=NOBM47fMvuZn= zH3CBkS|FiuG1nZ_jub^g4e{!DZ@kKwD=N5V^KBEb<131e6 zyo-FoLiKHw4fH*&se|SodvOTAQmnF;F}Mh`?!qQ3KLvbI?!H$*Shz~zo6CQe94R#3 zym)nMH*H;4$EkeDH~qjm!9D9ocwNLw&?7nzt;A9}Xf>%)2W(OHIT#tBy_|h!kEEf! zoo6#$ljL^ccN!LIpSXUIz*ipCPWrRw@pLIdmCNd?Zgl8$FFAT zTpEN14Hi;xKk0w?bDi~};w95{6aJn}_aJmpot&J`gFJeL0y|WqniC*=M{y?`s6TcO zf@+0k?cfC{xQ#%MF(H{+c!%F~i2=!hesh(Y!T~ojc*RN&+YcvMzh9}ubtlJ1^R$|j zqhh;Ph|FY#di^QalIrHV$2eo3RGdDbg2vEp6rAkh!$158NQ3VJRPARSAQVD7UumJ{ z)w}`wixLJ@+ikz}*tk(Qc{HgB$NB&I4i$!@Y&&oBy4XLg2rV74F!|L4-6NYScBuSm z=p`hbW)2P+4=O?Qe)phl3}~qqYUvnhnNI=G><7dGXdxd?H@fy`+czcw0{^R{r;|9WS0P^iz}FHA)$L2{S}ZWR}CA4&@gZ=)qUl3d;@j zti?U7$$*}&^`g6JBM#ui)j!uGio>3&^|L_Z84iJVhl|amT1V$u4su8!V0hwnSm;9f zI_oosrZ2Qzvp&R-H5DFMJqE5Bum!&S`e~WK`#JIK^#Q$#LRi>`B-v(=?O2B;pXn7+ zt%ZNC+@h(MsHFhT+)yc+z?A41*jbj1q;tVeSFg5v*?swJ^k4V2r@vg!zmS_ffr! zofk)HT&8Ovg7e(aBA>T-{tHi;gIm+eGP)K#=k$sdt2Y}j)CLs)c3bd)GyB%Lr;GK~ z!0e&#(Z$UupvhqYEqM6tN?~sSs|;Xp+&?O`8L;`EMr3HrwWyUetoq=64sf&wiLlJ8>=yKjN^d9>nMPTZ0|4E=?)y^GMn5!x)25H}mN z&Cfg7WI7ywTc?ItWqRe2G44QLWjr2kwP>E3a~SCc;W?Gy|l;1sgtPt<0~u0~40} zdB;0@TQ>dJ$gER>x}Fi2!tmkLakiOO5Qk;}S14{^G9@k+d*jp4&O6XQY2$bQ;Opbr ze`z@Jjwj30a{`KLx-<;KfQs~#DF|#;ai05?GJ<{L(XvAhu2VaR0ZMlu%z;fRF#+Qc zylSNROBqx+LYIOznk!B7P*G%bJz+iwz!MUcod-r~CY-$q`~3%ukuf6d_uC>UtcJd8{hYGsDKCGnlaFxS*R-ee6Myw!qajbRel zkmy$gBJB$x)d!N{pb5WLjya8+j16;LX3AyBS?H&WSr`A z*)n;8(?;ZPhgXyY{x+(Ct2XTM1c^fP6#q8+n#0!ZKwh>h_!vy`)nN`C<+{y{4RMaf z@NzM(^$}_EzW=oRsrU0lPkPRFZRTVE;3_3-l8^+GZSWqUef(Vv4x~JS54a$W`7@>UbA)w@ylw$f7M}56B3pS z`xLfP1=r|af9Ad#)%Wk2xR{5LDS}@C z#GYkq4nz*n7>Q-EmO;VqMJ}{gOPZg7!ShzGEx_z`5!m$QL(zZT&5q?7Ltf?ZpT;1^ zON>$Hj`D_%Dtd zj)J|9rx4`xV}r@!Zs0Bs+xR&3n~-Od;DS*HZLmk8I+t2cDUIhl6x|Ui7QOn$0zyIZ zOoKtmaC3?&eYhF4Tz*tgP{r|}AEeyz$aS}_=K zre>UXsCn~m5ciFi8^NpaRxd5{Yue|n7$3~e_q4o$J>phwaDKtq8Be7QdxO3>)^0}( zBzx!4r+EKj?4aaWzyIxaVK$5)^ls@aUEkA_6Ft(DgMh}keG-+Ramx?1gl1n}()3dtS7uD6gvQ6| z5chZA15W`w`WBxE(;7N_kndVhW~opIaG|c~eC#0ZZ{t%b&*&yERb@=wR{GEox)9V~ z_->V$CtSif(KJGfi8RzTsN-5H0OO_0tw(bNy6UE*b`Q|4iW&1$fWbW*%N@R$G#cWZ zY$Y=QytjiTLR?cvBe64L4eZT@4u|YtdXN3TT#4#1gWIJG+UNU)o%ghQ+@!LW9H5(U z57&FN^+nTpZHGx|2&`LfH#v^Em|0|(i*cP0*LJwZnB|YR6Tb=$C)p&b{D;{X?x!>p zjmQ0l@r<_W&Fr|~?d|i% zUK@(Kgy;Xlb)KvB(06&&=z1hVBi`iAZnyc&Dy;?@l)r1CZ0EWig=CpxS(}9Y>ZLn^ zzJg!n5A^i(d?pV%oS!we(_-`^!={4JP-iP%@UOCZ5XwC@aD}=4sq8boO3kJS#9?5r zT9js)QXvIu87qPBwVh<_V$EQDkOT=NfW;<;kXjCBx!viF`aF>PmX}yDwV3feM%OC= zXV$nZB=)rcB2WQfcz1egjFUix`DFNv8a9%<`D@xH0|gl^5*av}Ystzw3$JMaFmp?TKf)-MNuJJ6UAlyL%3dmI+xUfCn{%xnlSL0$Sk;+o6UI$qqFej0&^ zm7kkzt~YOTAsHf#Xd(ooQoMFVy3dG6FJzP}3r+sq2_Ng~MJiqu)HVbiwCkwb0{Q=E z4!!MN@NG(cKq*mra?s||YWps^4%kQ&_5F&{;qO@StU&#}YD+VT5oYghx!`Cf1Dj@e z8jN#w0BP+0F20)tG+rGT7H?iaar*m>hCj9rBMp-;=YybZ20d-Z0!v%7EV`*Mg4ChK z_?9&%jIv#>{NGQFf@pQB)FyPfs=_EZ4m%qEh^c*JBzXo2Do#Fic?oTClMu@SU$A2o zKe#vybaXtqCpfCEV;Q{b^+6F1%*vtqD+0b*o6A%MEdIg1Cyy6fZf3%$uB0AAsVRrwm6B_HoFnqQg)8lTalcqITk zu_BYFsZ)u8(Uqv!|I$|cwB(!~lf1`tur|_P$_(cAUH1;e2k;i~S zg2Jck-6~U>UJCw-lhui$Y-Boc_Z4?p!N?Kn$VOIt3&INstbN*g03veRJP53|zn%iY zoK=$4OB_(6`l)_D51zFGC95^6!qPXa_R|LovJmJKB)bR#Yv>{lg%7T> z1crPI4DkoTbZD8=!P+$s+y6}Th2WYkg$&sv}AEDFo zVI%>p%C17K7blQ5H0IX`0;OEJ|GzxB)`ay#j6!Jve7%W>8fteIF)^^sHvPruqWP#l zpmLObG?e>MrGyT8X5R(4>BVXYji$PYtNZHXgTc!$-k0)|!*hbSe0`0c~AD!5Z3XaHoAs z^SIr!hzU6=KopQ0&EmSxWp%#7HK3ynjMUMn^Y_Z_z|MRkkmxFEK)!BbvPVDp_Z=sK zpW&K{`6)$~w*3yB(v&-ZuUlQr-6d&OoCgeZt`n{v43g*=dmy*QbI^IjpDUNO;|v0L zHcf+70;I7`*T8#AN`BTZ(d(wt+*@3U>27vB;mZj(jg3#cYnj%(!!fFzO`M$qk3UZ+ zY7%NMsiENh2|W6ecTV~M`D1T-+fTe1{n4wzyvUZO=6KCbY~dLs1PU6?pk7m>fRFAR zbF3sSd-wtF4OA<%9htI4Nq=};C$W!jty9P1`FGj|T-m&W-6YT_k}txPgc57MMiP0S zus052{3C}a+MclIZpPe;hr8!p^B)htW-U!TNOUO*oM8WLk^0@=X=Srs z26o>T33*~y;6<4$Y8o6bz)$C9Vcr&25po4+7>1ZP6D zPJC-0jp1|MX=RclV?d@YE$P~J{ZsQq9`D)`=8T}E`?TDFlGY4sQ^;p_EO!}Gvyp(p zGLq}L&DRq{az70I&9CP9hSUc6AZR4dCN&~hlN00!!HEZWvv^GmFNE2cfeo~=Y;iHr z&bw^Y1agNvx_aX2S6PFh-M*{D!MinTeyZm^*e>D|_bipGhCJ}~E0iH&9oBJ^ zXMPGByf*H=Ye!+Zrme}|;%xB&c%eASH}_F=yLz+^(9?%>D0f@I5w6ThgG4>p7jj({ z&zEg6hys2q9o$_k&@=-tjvIxkry~LVH3QnBAfD&;U_O2%z-{De5|9e;sfTR0%Z72!Md%$eaVmb*W}iM5zwg%YP8JEcWETn<98_4}&R z8n-<91sfe;X>26VztI>JiRmzEL05IqMZv@r29?XjCJ=9q?;B$$KZDDd4 zGSu@0&O+X_C!YSX-7zZI2u8oaAMRo_(TzkAd z^6Ndm9vtux-%h!3hQ+9|rXB3RKvM_$rf%wmV<*}!fdCpi%D9hWqOGvn_8mUpUwr(r zW<#redi(h?X-X#75l__kJO}h7xnONl{aQKhLS3C*t5#b|#UrUcF>ioGp{?By##6V! zlOE4Tn;rVN+^Ug5q%iQP4_?E^R{~-CjJeI*zrO{geNoH4eTs@gCREqiouYGQP6((e zpZp|oZW3TC_SYv2$Oe=5uIBtd$km=kN4$Of-1qNC=rH&s3c?>2Os9x-CE%bTVM!n6 zdip-8XCgKgo6R&^TAg}X2U^|R0}SDz5vXNW z8KfGR=`!P3<&`u;OR<`pf(!ZOSf&}*E)Qk!mZlgbTzd{m(_~;R&UFE;@$}o0yJ@{o zKsO>p5NFHZA+sLR`1#H-Nn04!!C)yU&$x2+ zI!>-``!E(H{;YsBW5JBSRB@MIEN9;@8ZYs4X13{{&oMUF*_OsjxXB-7QW=@2$^I72en`6*{^zH zL7V}GavVWN!cxBFeuL!6ELwMhmv};)wCQMD6Rkcm0iUS$4-{q(0Q_Qj4947EKfos} z)0|?o{p=^ORP<8^;d{_@L||Y33Vg0GP>4?Sr!xOi0dRcs#6}xaBLc0mb{#o-RFpO| zJzZFHRM<#QJOE8>B*@Hsj1$;V6||WMoWMBziME;-=YigI*;DLUC-oj%mx8f&fsRSA zWx+Ife{n{u*_%9H81-TH5Z6#NLt^a_x{n(uzO1lIKv9i z8scN)q(T1bi4xiEVy<9uNlDKz2&e728z)Dd_A)8FRr(OWf829;<644%qSb$b`+S8N zzq--=SWlM6?W&ZNURFHCb0zb8u=!7^ET*B&!XPZS!qf+zk^KR|VBp?kl?iXB{R3x< z2gmFwpPMKZDM9mxtsYMoM+L;?`h#9Mt`cJJt;_2W9;ON1mmVK)Z_Qeu@xOXer-syN zb&Tw^fZYIM1v}J33}LNJTFpW?f%WBQt|IU`@EiTtt?#Z<*q@7j^F6x|Z9hNV7ud5B z_-}{wF13z_gWsmT;^cdLJge9xnvrNL&~AJL9Q1T29)@Z*1lm>qnbcZtVvL6wE9=aO zM*N(F0ufrwI+>&ifC|DRU=-C|=4WV$yTfh6d?bA7d+8VDWQQ{&bqy`8t zEeVu5cZ{V5iY8=im9lKn)FTF3rrlY=C;vLxcx03-`dqHaJ_H? z8uIvC{P(p`(@L+-{6eq5T8$%p%Vbv?A27LZKLPCJVs6YD6IVM!aP@G1+7*y#sBR$XR)M8Uf z1Dnk-q>D|;B`Eg~rou&59w0(IF-r4_w{MfcOBXsU;Ht4_N3|2Yp~Za!urctH7;qi) z>WBIam!rZDtfP9l<)E28n8{D-4F(Zf7`^)grlQi@t{=y+whK&yb{3LK*Gi@S(xq+WvP&eVE=f{eL zT=#DfH{1l{J!Y-1VNS$)?x@l;9N@J4Gtnsrf#re6sW7(jKur!xD6!1m2daUg_%3u=+xOuv_>bV z?zu5n@w8S`KU{`R2mw!C#@pp1UM*QUj+^JnPYFlY?rx%q4&dWolMEy=wM3-pq#Olb z-fglWz1UT{?VhCy+yOa^Hh4>kcsKCz3)nHl9vf3=7#Z<-DrpA6&5M(-0?FY00_kyW zV}FI$Nj@dukR8theZ0Y79FJW8(MESpHwo7MC)66iayEw@+8s_;8J&Q4CI9XBqh+ZV zn8h^B4@BV8iICmtHHQwN<6vE-PS)2OI|vjeIbp{OTnF4hr2VioaIp_in3j{3-kCOB z!)j)i*kU8fQ5p2O6f9v)TYB_Vvga0*Mc*3`gK_9qzDCV4#FYdQlF4Eq&m}LBEVh4P z!&U~%NkeB{%RFVb5?Jw}`Ctg> z!7V}F7W3gsKKrL2v$^ULE|HQqvWl2zR=I7UP==GsoT?5Ey>h5rWx~}jtZY0+szJB7 zaCAMAXNkfe@T$Mi0Utm{DDL(caO2f6jYcdOk>W-9&u`-^scO|2C&=0$EjLAbBnm6Q zw^+2K?h>OgCP5WI$l)xqZmhku-Xk$rO3Zl{)H$D|GU?W}Yozh)DT4ap5FSib4D+^6 z@C#IygFWxIAWi03qiw~rIeOP^v-VS<@j!~xy^jYL<+U7nfW@ps=(@$U-DQgs z;Y@E@=-!wzBcc|#0SoG_mI3(FCJDt!cDv2|r-{XK@x-|FDKF!n<+fNK!XNM~6;$?U zwZoY1zeP72PISw@0%S|nkk=)R<$`o0qVN`C(v9r*JbuEq7I^d0^727B64qdu4Q}(3 zYOw|sH&;W~nflMn0Ps_ZtoVp!>h1Pk*Ind6V~cuC1@(CGnQLeCS27HxSYFu3)Lb2M zH`{IUVa70$-k^YVO^NGlwuCFvOJ#o|AfRmBM)u!p~0Ph@(HTA_oUF?$!WgCQOa+s<|2 zVdViKgwbnS3ox5*@)d7Gf;p}>{and88^tnP&Yw*NdA8-Q`?s)n zi}Ow)vj-b#!sA)(>*q!$+;W$n$0G&13AB{f-yCUn=x|LsTN%_eoxcLwXDHyPlesh_ z3HBb@1NT9VLf5+TAZYL|f<)*DCyMO5zB-_F-~dAM)~$Q!uE|FbA!N5CDY#x#0|+z`E{228bBS7Io$;?{R~gU`dGq zVNN92_Dm3?x#~#d8T`$5)(mj!Ol>23y|#HTlSd7$vzb*sgC$#Z@LK2hBkQkhCqA+k zXm4x{%&AcTzuqSj+wd84lQkA&5DQpYWPWPQZKIM?eJu2uK6#mV=fap4Zdbt0WM(Wa(2iS%0F&`_w5F=%OWRp3ouWwcl(e3-H`Ub7VWb|kFYpf@ z^VNgD0Kd7_QGBPBvyJF!=|wSMui6m0&-BQL-0^noMjZx=r2qA(QBBvX6q3%4wry%cSBV*;K&i#atag$G>Padkj(%un<`nVC-ZbUBLv)&PyMjoW3F%=>x;`9kK|*-`(CmQH(H7 z&ITmdA34A?+1wR}JoaLNcw%5v(XS*-S$wxKdFJRyp7RIjo)u%pj_jAFEC-AIbW}HO zk2XvkM9$E`{`i%!vjyIp>96PBpZ7TIaIS&4U`ZJmxxnUF!ua>2+7syNy6IQ6P{rN( z?L?d(Ssr(Xh8dw8d1sGnje$PMT0{SMS>;+%vmMAL6H!Xc=#PEC(mXyAxnD+RlL?hb zfr0BdIb%C;;In7U;jfXiW{-Ff-F<&Qf>!)}#y3AUJ#G!ybElk2PqSdEzWK!_Z#MS{ zOp@Z!uGHh`i~+rL+Hm4IpMQ&U-%2IjE4b_9zL`whk&TDfxr*xSusn3=#3wl zV6TP+&{6P~bbk&L)pLV-W`|8t(Y-@)#N4`tl*bQdEBIWc<)TauOt?Y`j8O|U5*r^% zd&FqRu3wh@1w{zd$ce6al;>Eu4!KIi30xvicuCA`uEN{A6YRCWUT{byUi zJ>RFxs0}y*0z6=z38-Jwm}3=Q;d@~JX!z~Na)A3}m^LTZIxQ-&<|Rf3BJeJI`~FyS zn{-4eKh zkQq*7ag~IZZ(rvDc#k~Mj`hzuO)oilcBiw$NQ>@1Gc}7WFjf?}lnbgZa~(`0dc|!ngmgOlI}$cMMceZ)S1hJY&j+3y1sexkM|d~`WsKNvKf5#AZlZf` zUWuuZ=JqCAuFn{e*~l!e1-HZ&JXR-BED~1x_~7aTu=o=xq0G3CeW#CZSr1l4JKsDl zvjcLNC=g)~qWx@rRTX}Wx+SuWHBuB{Uto%5(;|4>^fMMI zizE)Lv04;@+T;2F6QS70VteD$J60M0Sm!GngSXGz)_4sVhY;Kr7@nR<4|f!aAq`c| zdm+eS-B|1*+Z59)#_$NyPszy1JZ)(X6wor2=D-2^7k^xqto#ECvJl*4|HooydK6erKciA#RQY*6 zk-w_VqiGI1d@a!Jmmm7BkbcUJ4IhF9R?r&FTCdH4+gA#8t{9J~YI+pF?fns%1iw@d z4at%C*%B@QR1<+Q4Lq&)7P+*KmwpfscK9{VuJ^v4v?lC%A8<1U9}7Ja=VdF)`SSpL z``)GM$M^amJAiLg+TVa!kKxo9Dd)`-6ar*fg=aOI>wP*#->-}r*C(G%xWO5v+z+R~8u(%H9Yn&cF;q?>-^C4Xn z_vbHm-p!b4{TTBwM9XD*4B;0?31=`Rd+k1Pb>~Pi0+S_@i?|Y zu$zD@bnB1AHlLs~Ds~`?WX|g$$Ptl#FhT%{`KKmZ0Xxz3dd4fG(RoOpHcI5t6dU97-e~wK;;I8gr&Gphf=EBaQGg z(T`dz(y3_9U<&N(&UJryI`NL^!lhC6S9A_&_j!2HIfg{4%FZy+#~GCi?G$EgG&c~wGSOaz{f6PMB_Alhco>WZ^}x) zEENb0kM{ObfqShgu2}guV3ID@>JdemiMMX*V4TFCs;4j$hOM_!?M={srZpz2Q=^et zuu5r#bcY}iO2JLmnLd2Rd87g05SJ;>ZOK-zZ0(8yfusOwP4Xz`b-RD$wYGn41Ge_- zO)q4QGNyWl$cyO~%oY%zWPo{N05mvppvO)P^jM?eQ&=TM*3EdGN?n4s!3o!5NxUyh z{v|lys3se34m>%;owK$?pm`YVMtg7mnxeEJ*!qb4wN^`@P|iQn=zf7De_h#M*FD*8 zVKH17w)vx4REzEkejZkTr6@AMSpNzVWt0F>8t@^qUvgVKd=;GaJOUujo6fdL;@5ho z%0V6ke;c@asZw8Q?k*MBrx~Z8wnvP~TB5kD*zGSf0#Zh$l4gClqWDzu=d*3|ud5AD z%cvZ=01-j@#qHcJJ~fhMk`n%rLet-6v_Q_u)WO2(&fFIC#e zVorZiYJ~4@d_4ElL=h>bbs*1Q^r1ZI_*W=IB4^b<4OO{GIbj{68lZ!refB}f8Q-S7 zr-GK}ubD5kDOdPo%!>L*d*?l}MaqitMSozdma3s$rGXdQTnM@8SS_dpj zJe;rmv@Db7MthFcf3%(*_v&84i1oKHR$MUK4q~Xkk!N(Qd*wRt%;kGt*#lj)G8p;# zNVPT#HNsVA1fl4M#E3;F=ZEsM_p`t^LIF4u_=^0p9|rEWw+H8ZbFj@GEIWq5AIgAI zTfg2$JZB$Wejp4V)vpis&K_1e1D3gvzmd?~Q}HQeu~xh=Lpx4`vG^ToGF`W7okr}L z(v;1VMMgi+2&baeeGbAXAHGj{Ds7r~zw2%WLLJ)f_cX3v+vhe1jGNp>lTb?)W=2o< zBXQSKiZuJoojr7Yqk{a55!tm3_^pvMAua%YDAAknG5vKU+Cle+q|l}IpkJ}}1z3tf zPJ-tOR||2lm*&yjE)QKsa=_u!@5WZAed>iU<1jhgjPF(+w3~#Z4mIbSBmg6pz1Adt z2EVPZ64>nNc*D&~ZWZ85^9nk7jQTQvt~Fo#^@jffx0yN^EG3YNEa8q#Te4al=8qEB z{jojbD87q!?;))DAgsAe_${T@3SWiA;aavDFoE#;K(GrI-Fh?q_D4sFa0Fusgv*%Q z$}$#lLSQUcdc%jO{7gPYD1)C9-E8EUdx@nu{i99apUH(}6-^33c?)AN#|L}UT;?Z1 zUQj$<+Kn+M19+w??TrSj*k@wKOnw{9Lsds7XQII%A~seOFR1wbHzxNqbpHQqxbo(+ zxn6JWqJLtc7V=MYvuw+7sDawpiW|>+?^n^CQ>5)IM?jww496}Zk}I|K%X6@q-%Ei2_w{?!~U~gN3 z#mWDTNo)!kotv*REe-4lA}UAzB+dZw?8!L2d7KT?>Tf>hu9AoCGIc ze--CPq(y4Loeojn!m0B{ni9l0XKpNtzF82aV<3|#6+jtbcQS?9iVzM!WCX7%e}K85 zh;oW3`vcG3qJ3m%alx_&AgDheT?e6A$X=b3nn7GRB1)yBoH=5qC@HlxU!^B4PkR{Z z?W!g(fxqD1@A_)-F_~F7^f!{*$*q=21Hkz|un$P$-}eV^Tp8r%gi9~-|LPXYlD`U~ z*j97&^y9P<*X}zLFU%BT@#VS2@u)m3gKZ7XJir^>M_`)yUJk4g1iFhEeIO;!0fNrW z3y`P#&3kPuX;PTQYgnKC{TRTGM}g{sg!8upu!W^@ZU2I{rWMoze*cT8_J#Iq)m|oo zNAr5Na2<6kb6zAS14X8qJHiR>BZH7qZKwICjM1h_j02#})b#F(I3Tl037C!% zL^Nvsa*NFg`B}!)L$b{OV}C-J2EY@FqZH6pToUNm886LzTIG#Ec|7>q6rz@kz!}R$ z$#e*LC323OcNA8@dH!APq`H$ybKAx5oWHiO)#D+kvzm4QITx2roP9M2TVAPls6O_g zA~^-sYa#)GV3!FhlUi30i#U?r(nS@p0zz^(4845>IaOnF!5aLk=bySy=brsKdkdB3 znB`j8q!(~+m*!w@j5bDpAySm{?W%C+4^Qw{t;aL8NxHz}XSbvq{0p7W0V~2>{@A8EIGHQ`rAfEf|tovl(LBCYEEnKS#FqTl3S(+2BcERT24b$t8nL?AYT=Nw>!z!`~z0>vLV7~(zs z*dF*=oVh@ZIax+rK{cUE|jLI}3)&#r?KJ4WTEU(<#v5 znF}KIxJc>#{5@QA<23So0HnK-Gw5oEDgbXi)|q76Z8_~K-Trg@jT>o-HdlGUiIMs} zUBLy*wcEQY>JnY|jV&O6x4?yAl+W!)qc{3^U`9ntBoA(QC;-|(Y_BwgqKELP_mRL@ zc)Zs@`^pqZI`~fXlmxW2>2(~OD0CRWCi=Tz{?;{wIgHQa&%QcF;%40`@A{I})vh2f zXzU46Qu=Oj5y)UmAaZomcsd+kNj;B4^=*ci+Ev$xb-5<P=2=$!k!^J1_7#UUbu z{)~o%tPX2oCg~cm!ZR+NOk1PwS|r*%Fq?;uIRsKoG_y}5^zPe}fvU7E&h%!~%EL)n z<@eOw8S_Q~jy9Eci2?kWSMBy7=)rmNmRNQHb2&g+38A%+)9Q9%wXz>g6K{HEB>L*Ry;%-#QJTI5X$VKm=)rp=Nzz zS=a3nRIuY?yMsJTqej+^V%;@MS84-qm#Kw=#wlmV<$ijs%XkoQXt}k6gdte-RaLm0 zcHqAkb0o9c$GqHLzz8@X0AK(nhBSS8g9W)!SRuy?=DERk9Wa>3XF^Yo6UZVpZ#O2_ zo#bBo{|&>|JawDMn=KL+%Y%8m#=$yMYSG4{mcvc>P%nEUd!W+3-b|6d;QG5^{#80O$Z3k53u$PX&x}Uw7`ltc>qpOfCYH=f*^H%_P4q0#h&(XB2f3?-?DY zfwl{X_L(E((r_xX%D*8HXdx0nJHu!8;|_M+M$@PoUdft{`q!w3u~o9NvmZ*IHGA`{ zVy+8DHD=pwH2{+a`Ogn9y(kVAnE22a9M0ddIad#y(2(J#BYj&v0_z0P>_}NC>yk#4 znor1DC%#$oa5izZRm&q7;1tXJ-9AuJ0T;0en}%w6h5>(E_sOO01+tJ7PZHZTUG)igWf@Aoe7k?GN0YCg~S*(1C2&wA`+g07tV8z+6 zCVMQKm~(len`_-4e!cGOME-f=&Y-+zr$%i3-N@qsk6YxYt5r;gK1N4MkqTQ3h(2CbG5}tT zCYknp0NM}!fDi09=V2mxda~x54qu6e*_jbr@NFsCyK2J)njfg&_U$#?^oE-P8>WGc z&WL5@CcqS*!$1{T41)Y(hwyr`2j|_aJ{N4i(dg&Haf2h4ty5_i&v19#?qQ-c*oGfJ zWC^vs?mm*0yi12G=%=}WO^GKdXB+;IJ+y0qCr1O`AjF(DQq{iv^mSjOv*{6#k@fry z=oon17f5NvTKDZuKcu6hgY)+xRNrXfp^@aXu&s<0H1@@7(4gKt8q_2CZRVeX>kdUp z{Ggs%pIBB3;=Xi%OG@Mn0vqH%MT+mmUvqNdb)(^piN;$3N5RCPs`W8%I{ynBugmAJ z`9;vX2)sqwNCj(Pq5;yhC(M7u9Vi?@_x!a(smmL`W$8owGC2PSh&z0SXn}St0RR>p z`|fh+%5=VjTRmu^N4UhQU|(*)cMN)f!jmwYM9e__5gjJ2{zRWMjvm#0d5wXJ7PYBO8SB|2gr%!X58A?;Kxr#>PRHIhw zfOSfo2_-L;+5|iO7Qfog`1A-dXvOK0`h!8h1-Kqb9nn!-ZRX$@CQsC3Bx3Mka+`-hv{Vcku2Wi#6*tn=8I#I~WPUA9WC{ zK_44`Rackt_N7*vU4(lc+Y2E}Ab{L87**ntJyx04l`mQ8X%=Fmh6k~(K{apFHXctb zx|!Z#tUvaOM3Kun;=|RYP|=ec3rATVJM&2LY9`Zg=Q)SYg2R7ZFLXmz?n z8C;^h1GXa+?oLG~`3>S+{0t~w@aa^2>r$HW##uvGJ4m^JaXG<&$@u?|ukQ?qD$BNJ z#;^7Dn8r4MBH(BX1_TiW$#yHCqGBXxR6vphNd*+$wunR(1(l>AqLL*RSxF*7QRH0Y zA{RMR_3cyG)V(n8{bPE%m2=NI`|Q2;T5G?bnPwL$2~}X$=3d*Tx6q$8ety6Q0;Us; zGsVXL1X0N$A%xkZBd`*@T#Ek?n^-Iq2f=CPs9$2Qys&3&-l^1$0?rc=R(Z`NEPR%K zE-G78a<4&4#=S&~mJF0QqP~T~&bhg{juok{KnDZnGWJRsx*aK&(DyL+m%DPd)WZ^EXPIS2;vIv+(KJ_7S0?Z}== zv8v8&eU3@^k2WKakP`QIIDontKElC}YBVwIDkE4N{4X>>ZI_hXx)r7Sx`f86z2_H* zV+Wp}&uV`c6mDAm9%~4o_u~D3p(0ScSX2Z_i-p$d4o`3)(abfS=zWoPSAP)vfU)Jm z265ol<%HLG9+n$>0`t`cE_$jPh1|ffZh*cA%A0s7<+ujvmZeFlMk^R7nD;P2Cie5^ z88oA9T89XJ8`whY8yCNgG({6 zZ-!)H*QX8373K*;D;9Qp3RFEqS^> zQfxEOH-MJ6sAMj?Mb3dSZrJki20R9fU=rqCbqPnLb@#usVnkTJIs)qu>F5bHqbAxA zqM+h<`$GVE=CbKN_kjb4dkVg3PgKBTs5RTZIk1*nM*sKSQvq-p*!ilPUGLjW7QQ2G zM6iQ?3Rg;#1up$vN&_-8r5x9!-s*<;)KU)3Y8lqHdo&9sPw5A*;g%nVHhv2;DUD*B zJmlDP6;VpBW`YE%!&hqfzz<%6azQu5`)1dY{m{a2oEkjY-fV-DAGN=8S@8-=pN|3- zid{k8>CJ^-9LEh@e&#D`2v|cS`*ph1vwdv${A^D-4cZi~Ur=Z?$s+EcE}IOgh9s}30AND$R~C5G6}8dz$_3Vc7B zm8mkUJ3ppy9>9I%U41p^A+M7$7@|a0_jFDcgGVJI0DiKlH;1u}`;`Llk?+dy?IzPp39hgpFDb6ZaMgIEbzf+Pz5hBL_eyH9?CG?%}7nd zm3?+Bdt_}GdJZq(Sn$xx6`=Ops^wzWtnZH|OFu98)~wK9Ozhfc)wVE;3;o)tM{isw z6|rwG^e^E(k%6~QIMV-HjjApWKpmDQ*>Q1kC;Q&|pC6FfvN=X$JM)CmrP__C;N-T< zK7qUTajPGHR_q4?xq_@}Fu|gEBAb3|fDXo=-*uA@D8Tx#g?D}kFbxNMZNunyG&%GxU|po3pD)@LhuHR8G zJU@tY5I9?b+P%&4^cRO=R#c@%i(zq*D5ISZ3v-^5y7x`nmiq0vx!PDgyn4^DmTr~s zTmEQWCa5Elz{E$@=XR>OC$9NP9=Zfx(%Ir{BzRBaB}b!)+h9s>PHqhNa4kv!(uT21 zhhW}{2TUw@R?97AXSSQTFbbRD5f9bKadC_98SN8Dwh4aV*s6bn(f{79k8nnpdZ8;N zzAU4iq|30b_LITUxEk}dC$`q5cX+DZYq%9Tc{}8uig%M~JT~R2xD+y$!^-;AKDpFr z9GN=V`x@(%u_uJh(g<1{0~xg@r*L^K6>h3w>z4-P;*T{Z)0~UB##+hFACeED-!}L-)2JIx)mf194R$)XxIt;E!>r7$9Wg2{LCQ+KxMVj0rS{H{;cAbdFaR<&d%WuAj`d3P(_k4D3R|Ib}ERg@Y z1;Gc##Vn|UJHU)u5G8XY17=OKcadXh?N@BXjOl)c;;A8*5v!$XBwtr>Do6K%!)ErhRP+k(8j`&y>Jj>rq z#?V9ce(if^ub_Aa%)nBmX^9uYWV4@L%AA~>EJTiOcgDK_(}SYnYLH)d>V74ILKrsN zax_lIFHlGu*SPZD_J17q-Jn(`f`LWz;(#tJ8mJ{YGoS)2q_TT`Zk5ojb#k3d^m|_W z5eR-)!1WNj>3berN{2hfqGu%EZd#SuE+bo?zfR?aF}87%!}#tmE&};({4}5E{qmDo zn8WS3-4x_>u{zlqN5~DK6bRg`xg1y15kbLm-OTN(cNe`tUmj3Pkgko@;bBf@(O~rH z2`Ag&drjup%q;J0EUT4cYi}=LdDMCmTu;`6 z1r+bxcvzCTLw~6*Z*dkFI?l15)_ph7OIP3U1MEOACnWSuQvrRD;CP@39o0Ptqk@mU zW#~dbv`BT6=M9|la5{m?TCi!_^6*E<<9f1(bu8bl@%4tZd`NxS`8@?D27r^N~Tt-M=)>vzH5{_ zg?`}brZ%2Qz14KI+hboxb-e3|6)66`8v2a8vTl zCI#ge|IWMwDhPJ+%kh>C=QzKsxJnKfDjj{j2U(Y%Vx>Q+Pbqn0|&#DQ1{^`pM_1=rvn;nJH&c8nDb z9P>jz_zL%*n4+`#CpccfhXeiRXt{mScl?YFy|<{_2~S<;coTRfYOGlUeGFGRK?y@u zIdwh+#7#ez&Obeoju{E#rvG}md#MmQ02Jey8b{iXt`R~LW9zW&Q?9#`0?e8UUW(%UBPQ5r*1l5h+_@VI(=UynQIrLA_z#M^v2yslv#P*fEh^7}la8Ks z{gSDnPo+W#!0ZaK%jf}XU(iw#;+7TF&+rVm%L5)iNB;*Hi=ipW}Q^ z?~_|a=tQE)5q5AoYyO@p&l#&S0oA0rR?@wdFWdynWti2shx8+yNo5mJiF2A zz?|b4WZR7?QjMB+P{` SVZRwPtF-eq`a3>+hktMC6v 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 z zE#fH!4q*&#GsGv`I|Ksc;?BJiQ3#Iyi&(QX>Q;C%Yy8ofc8CF*4qWJVOa!Z{tltV^ zj?X=J<+ReAoSpTnqWz>^Iu->Rgx(m`T@t6{dey+L%ZARf$F-h%f!Qm~^d!qV0^XY?IevFffJ)D@FEU9xI;;%BpsPqu! zdSpzb_rwIWK5J=H^x3KiWcxsF^-pPugB<1t*9NQQWY-uMYXar8?b%3;TU>ik-;rp!`BT>JE(F4 zZ?DYFQW1c@h}%a~*S79qOW(jsHo&KX6W9Jbo~Wgcc*>@bnR%Tg5>!rQnQ<-8jAW#x z18uwz!2|4(4RtNIvFD?oYy6iJ!@djWb@*dJ%eFdbG~W+Bj(}P6(+Uus5?#kueY23p z<-m-AJ8Mf+Q8qKD6y;h~fd=grPurM(9K#V_zUGMauWA%kn0i0e(cb_#Qo&dyqO3lh z2}n*lWwMf=d|5K`)_-Lg!$pYtsB(p?iO-j0=HgDiP{GVg~@x6Jf^d z*y!eR@Ck}8p-KeEn78*ta&i_5Z`IBRr+kE*d7>$aEZHC!TmEzlVtttNzrL`7MSjwz z4^owQfKQZR*8`}a)4ua(CEUT9JK{Ds^|M<6GRBa!<|j2#vn5Myx#d#lp-!radij&1 zdkOY7;MILl-e0^C$}wbgLHw3ir3UAd|^(BoI+2_TuVEVzrRUlICt)w@edb*9ryn_PK$q>aFtI)6DGZL(L za_naH8igi^@~FvOY5}}kzDwUCoR7Q#^VH$YCX>E56>hV|rUf%~pSBm# zgwCUZWc_Un*Tvmdc)_Yvkyo9N!IYw%mUps=qH-=2*M?(0O$Zdw_=yI=$m+$(;aU@o zWIU9IOCN{yFd*-y1-J@NIt`d2n`H$fXH^*j9gP4KJ8&U0qxkju!+$)rD*VUVk4QD( z_hiv4Z+-=87G-}|KZHyDIN2OP|C6Za*i~G5Ub6$G3cJRj#y>!2kiT&*^th0cMJCRi z|N9p+iN0ixW4u9Zy+y8wG^E4^CMrdi{(}}?lft*c3T{VaYMg-E35aF+xwLWur$vgu zRyqRBTJ*a*0zmA*F7OL+={be$nU$ih&w2(1{6q&cG|~9pXRj-cgV`I`**lKO259EB zZ`m=S3a0tfqf9j%L78@DX*6qmx${_^%`Dl`p$YX#v0T7Qv1$RS&`lHXnX^JL>j~lk z1{>smn@9<$uxn1$6@UHJr*f%{A$CkSR5B$H0FUWB&UCSD+@B4_Eu2XB_6N>`fbC3r z?%JL*UmbG4uAu}GJI0GiDCqpIDn@F_AvtR(SqS2pA2slME2LQX95&2o33m;GLG|>; zBZ-q09BUYVX1SfS-KV?!K^UZyGa>E|6w04m#LpYb3~at+Snjn^zXUWC_ex^*Uo~pd z2MO|1kdtF@dNWw?S|IOF7H$^jYrJP$* z>>)~!bdcRGVHX5!+09tWA^LN@!Y8vCDaDowBM%x!$ByV<|6CrL3UC=H_`}FJmvdL* zn=By-5{+8e^+40Y13HZFol<9ZIo<$}&MBr92RH}9VR^7!J?=-o`$8@W`s@uyrBnRcqgynuUc|M&hgv==-Ir5&m+>dRieZ7CT2Ffx z#UFi=7({~sJ6db$j0aaIxBKbW`EPeYw1li6TfdKgQgh!7Tn2@iF||H^O}6|_gfgH0V% zDXt>h&CN4xxBYvi=qp2#-e`(Y$%1c~gDOv@nd&wL;y?g#NK&(Sj3PSG#?J{`iCvas z^u!!tya& zhnG3r#skdz|7>1?I9B^SD@ziw>e!-nI@w;)7s#xpK=<%+rMG0M4cUQ%^{yfvs)GOa zh(qH1z>K}`rBEKuN1TIQK?09^^h?HhhdFsXm7 zv|Vpav^t+jy{?D|oq^!kk|E%5p9h>{cv8(d)qMunwfMVo5vL3}l2dD=bSa zsZFgKxaNNVaR-bz?I`L{PY;rz2vO|4G^r)j z{U3uIGqI3sU)5lA2B<6`OG&&1VyaUmlF?NZ2t3?*Elld<YEah_+&e)>P|3v{Rr$bqI;Z0)99t3~~?Nm=`+Y8oID*aU)1*FUhUh#!{ zTOcQ=K7TYg!|v0ltVX!In}MTaWa-5-{Bh&ONXl46LN%AvQpnhMka1j6O14A{zjw<)rFumXx z+}5spyh^@1akx(KkhkK^QWc}9qNJnJGBWtDJ=p{AI{d2=zcF|G4-#F;EcmS>K6D~6 za}Esd2a?VI>6rW)15#BRM<^3t=3(852b8z_svQn&IbDZqH*l2E?4^75nQ3d@+1pzu zNAfq%HZSh=3&98`pFfe5+T$++Lq*(dtNN>(LdAWeR)Ujj3uLXXZzhG*H(r}ttFTQ- zqA4*gk7diXrtqoBODu=55*Nn2`}*M`S)aQnEp?v`&NGF@G05dDh( zHtL%ZIu1#(hk*iSK7IH1ACKmfLE`+(mDUEFGUZ89mOn4B3g3?5#ODKUvYJkv7 zs7ZeN4bJc;^1Ju@=RWhyGgouobIv|{uf5jVtw5WQccpmdIJZ!Tvbu}UOD7$j{IEMX z0HC@VD-~5$RsXgWQjpw;AW{_u{pZAImspI;E`jWrka~I#mLD|gp zR9Z&@vPZA=c%4~<$mUf7ghFoQa`atyED^C>PCxbZ_}d?Zy|?$8JV9NZ$0QM&!YI$)AevM`{vN0;+KL0O`(e}ebAMpDN%M9P3%GDQ3_s9h<>q+;PoIS0vpbUhaz>u6LS zc?$w!K1l(}pMA+&>b?l$?;+-QHcQbL98u7{g0(8VqxrjwF%?39zfNx^2+D1B*1>IC zU9i%H?a%KG%bycrWf0e!=8|QN6s)w`VPZ*WAJ9|Mtg9q$LuJVeyNXDdAwvK<^h63* zm-KMau|bLDdj$WKVhj@Zcnn@_`$DfI7*N2=@w2GMWJLYVcYAl2u5gxJ*rtA&bj9f} zE!MuX%p2LH1H%KV2Z^rMhWuU|I@+OEDZ`>9efF$% z2Yl1L}cZ{cq;ms zuMPo{#j_)3YKYW86D+ecvL!L0)G)|&swTu_~9)NF3+MB1rHrw!5FhxQ?+wg|Ep_IVIe zrK-s3R&cc5wJQ$kM}Fi)Gk8iPE)&rylL?Q$JjT|83SG3!yZ*lt`})--1EGNjqKbDP zId-?mQBq$30czpx0aLOnDUPDdiH2}RFP<)Y(sN+>x?CVo7OB{ZMLREGW}MIMkbYhB zV$*IJQcCXnd*>0@kzwc<@A_iy zmp=6Yl8|jpYzqLvcy8OR#8c{Ohg^ zwrh2T={aT=xLV}3(+yKTeLC8%%BD`ZG72Td{;uyoA4_(kU8g@3t;lpAIbh>s2t!|j znC~~M(Pl7UK{vEv`B-@0Jqk^skpL4k=jM=3dV1QfdkVGETcI2RXDXp6@z<{+a1M zD*o!&jj8^YnJ<@lEjMLsMs)V5PI(E0{iV(j3kWh0WNvj(-NmV^v2JW}re1w)VJFvCGmETk4@HjOR2U0g9Oa@OXFw_NI$QwEH(1I-a^q_^%2L<&wkfK zr)=K8;`HDC8b-kwPt@EMUj+Gp_;b`o>ftP^UpzZzs*(a0_zmVR^e;){?=`(Lv%Rp* zjg28i7jar4GRsHI!~yBmxfYIYceS%iawr<95H|hr<|1tUa87dtu$8rR+-Ju+?PoiE z^QJz717gD@F;Od-!Kg)kc-p0;J$B1~7|-E+t}dlw51GQ0P6ZW2!@RD({<*@j_kwcQ zeustgE6ca!xI{_~aB;E63 zGmM1mt(liBT*g6C zOovi)!v>gN_5kcTQ|p);K$>0Zv(9SZc}GWwxWecF5UzJ2F_b{Cg%}z!f|6YG?8s4T zMQs55HQ20KN$f-8NRK-`Kgjc&dKyh7NR~@fZoRCB-GNw2lztl`$^J#L9u9=D`|tJK z!PeNCF=Vlsh%PdDo?6;lSWKww-W6uuDHN)fuql3d9VgX0#s)Mii$s{|+03!wTz?^` z10U7Lt4Cbh0aIzdyX4Z9X3P}WjqK*0YhF!mf({&kPy@fY@jnMOZ=f@lm4o40o7pF|h-)kuVZI*hpVVo5N0Ecl z{gCf6DkS*jSu+>&CoPo3Qe@`vqv9!>zL}dXt!;mETun_+;13%f+jVll@cmXJFHT5# zu{!y}iD}t+mE)TYL6T8mg`6kE*Ciegt|h3wkZuwOBRxm(rb?(e%mjYbUTW0LOmndO z#2H`;SKqH0i`$If509&!TOSNy?gGy;%Yo;baKIG?oS@nwFPWjTf0g%_gJvkv9Q?a` zhIa8A!ePMNA;6m8+|4OV`fc_cP*0d%69=t-nS+}++J3)wEcQyOlC1?AwqQ&CmC?9C zmA}C)lH;K~=P!o@^4zbWw$b}GYlr=pXIH!jFvS?poKC2@J=9FtE{8{g)#0CeT({%# z_$oOBxI+b@^aI-qI(?o^24SAcme$$iK9u21qz;U$ZG6az2V`~r9}6k4VNX04!5Ib?X$#r^ zyC>UuZX%yiH2D+lyccBw&eD6g4dp}1n;8_;hn*7fFD!T>v)%R`=U$6#*H7Oe5iNUc z3q+x|wQB%L8d`WQ6G{|0gD`?*4u+~-IT(u8qO|BFhl~63u2(AhB9wXSm#LLM|(NhJG{Mi+7O268NP(g`@XK%r@nDIkWF+HbY z{#0JuL30r3trd~W%@$hy*DmlQPU6tfJng01c}Wp*Iv3Aaf#*|Yv}laM8QMd?rG^>m zZ1!V`I0KL7QgY<-2OJAg#F<>f2(cFIbu~2pfg^wDxo@kLCk1_sz;v`oU9ih zsr)X8n<7Dcu5e_FtEMIr#OGd?h_750Bq6|rH1ZrG*<)D1Nm`|GI!YAZn+qctgsKPrK(byuTVgsPom3}~9R8@a=C zep0J!l?L}=KMH;PK1t7{bn+jd(Y@lkUOZ*yI7c+s6~H)cYS68_3w9({?@-xr`W$Gw zC;-A-B#aa^m>cg1beZ-i&;hs5Q>Rnqqs25RQRQ2B#6<0LK9 zwp#3VeUP|ARKJ2qj<|Ex=&G0+IZ5Us&Ms88)Olfev!weAA=~1)K4hN9H3vxUkW@7! z3Wj)xL@`e1=nbDun=INS^Sj-%1+{_ zKxiOt!6{se;Ql^@Q_KGkXlVjBsZ-c?KfGd|qhcx3S2z;2%tJ_Ws#6#Dw`n8$|9T-z z)s~>(HCNEA+~aB~wRN&7ts*N(J~Nwnl6dpz6Pdr7ZlP0L>kp+4)QVB^Jn@o+V8JwT zd_KaCnHZC1SSX!ZH1Y9wixVL7G=*^;RW)hNw5l4fILOLhHA5xw2#v?Q(mLS2{LA5= z1ka_qP!_t1KS_w;2u&k}mE+(|o4roxQrp^l${YBOCtcwn==wP~!bCtFtC{W~{|xsL zU7gV+j=I)N_e<;ZqKsH&DuAtVA{JsT%dP8^W|%LMSU( zDU^9?LHVpCKUH_h;=^A}$8DI4zz|qSq!_vn=VaV!`s=;95V#~DD&@1n({2g_a~n2v zR|NWBEA>$rus~9cJgzs)n`@Il;2N^{^kF9XZndME&|~zI{UV;b-9VQ}md# z!WiRIAAKu20#2px-PXV?nrpbk%VkE630?Il!-tX(oPNeGEztWxN-hwE)JZvww(c3 zHQ%qf1M@h_(yx_1kuE4K42c%m08<5-0~T;2M+-PFbc+$X2=RtFU101&-Ncc-V|F4HOOdx>=Yt31kQ~>-l$_u7Xs~pYcS^}+@o1T} zN63=9(bbpVQ>v>0ot~#UBf`AUw$>D;FujJ%{9=+8BngMviRo3W@A46O+Ox1mWUE!! zp#x4n2f&zQXck5;4Ue*qx3il_UbQrdJuxsl<9fND4t$CR5clA;77}BS0BkseZhBw% zlPei7IXTmBh$$Ze#Z6IOUbaw>{K8NR!%R{Prz8lvqN0nO&o{qWbwh&vf($`d$8JK% z;&hl9L^4h^V=cSdM&h@!MJRq@Iaet8dIv6v2$o6DgD5F1CvE;YdK+X6e?g|;k3dy{ zw{l*f3wOffzMI;gKOPE@ooQ=L}6wVpeLu=6{c@{z%ly3<<}rs*Kbh48)>>R zyTNr}<<0&sdS@asBP1Vxp(+B=+mpQ5_iT|o6GjwCVaKsGk;~uzq&?G->{_q$HuDkj zjh;ANcCP1lJz)-A!T!>&)Kqpxp4ZROis$c5BP^O3z=Wo!#I!5PM}{qw{B58y14|A; zt~a7A;8?Qb0;YIlUJS-4Nh*M35#v%0EgdqY7D-HtuI33Mpn||gQH^utfsqjnG8joA z=DFB;SD2vkYWY)x2isNd2kS48HY|4agqT)s!#suPE$MM>>q1kS+s`UL%6~=Wo2!G! zD!Xk@%(TOej`ngxVxEqjCnRX8SLD$aTP~U9>?cdlB@F$3`=!6hlJc4ZCpIK}pPP$e zJ&Xc2L^jg|24p^wX#-vxs=oee&kL?~v%?YOD}!rKLCz)9?nZONVnOgzz75lUvHj!* z`=N|V#D+zVPhsl9L^b%pr@6u6JvAEw6%Pjsfq_5c&i0VhwF6@-d)uUR8vFt`@# z{vsGT0B$|Qme?2w4Q>Mo_iK(`d;cdpA;mzlH!?EP6QzgCC41c(K^x1*{x27l;{8W| zuARqjTg937hWTM|EDKPVc({RNqYbZ?AyTl7d80blXV_`UWQ=4=UTp-KzqB$)JSA@@ z#~U4oj@+C}e(KhY^KTW6{Q1)e9s-B6=eU1&|G!6X_1PhkLX{l7^QegF{oui*GfDi# z4?`bpKO}>iQBcu&$PKGxM=t6g8IiD#KJ@AO$EOGGXNO4`Zh<)j8e|yDpt&#!^rfO< zbnxWyyTJWY-y9)Ke0(-pg9(!X?Wa8$!CL3lD)v^NMBb~;e~B({^CKe&xj*yT79>5> z2`lfJDN1}sRM`&#lS||MDE;?WktHDIH{RNBX7cPm;^aYSN1K6BpS%l4+);)CJUn_g2aH);kT(-tzY?l%mqBZ#R&gZ1 zVSe#Evd4(K9IB97T^LbwA~Bt~z43LqG)#ZKhPU91U2SYM!3hHPeJYw+vm-QuFqM$c zSe%4#-Qh-i2A0G}tZ6vpm0ALgIE(7Fw&BIy+s4P%$v{yJb=}qwO(oBJqcESt_KtD^vaz;L)WFRB!{XJuDX` zKRNl+42Fq;Fc_2j_l5QC!W<%QM-bU$l6|?j4a{F&j zdR38YLUkR$jrM?YjJ(|Jc^cXAH=N1nr56hcl8`;`#0`TpV3g79dw8!tp;MC!+2w|Y zP;TgXM5AN3p3Qg>464#5$WHuw zK5k#28HYpc{v>f^IL<##*72sL)YuJWj>qbZ1(5v4sdm`Hbn-ikXi!^iB9a852QZVv zGGD%lu?9;mfb3Zg>QQ#XV7Y&H7Z4{qCVJr}m4Je04V>YG>A8p%5|WayS+>)Iv*h2_ zgvy=*HB2JR;>ym+u^IDZL!)Vg;@Bi+AEL{7dCFISs9)q_pQ4{9;hGp9Z;Jmv9dhOf zf{yfgF((tINvc5s2w?ePyjwlu5B5w3m4vQuV?WqSd-T!9ctH=5TAZ<((vU0vbED>= zS2+h(z*0m2aM>U>LQrz0b_&E$(E(L2A+P>Ht7x>KI%~ceP&(I-Pmj<08y~{eDj|$; zuAHpos{=W;P$kRTvt`|hfr(G1Jo3ViyG)i?usicEPSMa)6yRu1%ERb z(ZLawc(~C*M(Zl*lZ6*EhI`jNG8^~fm3S~rC#4k$x#%Hf3&(T4BQP4850HY3r(nJo zQnuL)58t1b=14q!vQaSdf;*#xL4PiLk3C~{Po5e z>%!q&8UrTUol`u;)%oClx%$(>$nJKZ6+8dDDZM43dJOsyztW#c>X{&V@6{o;@F0M#b$8yvpG$GnMteG=&yrNbCyBo;`;~ld|87!^9|i63nX6!}e?G zmrsF-S&KC3sq`w%mCEXSm62e_JlJdmsrN?cSgu#>4hVD`g%Pm3YA4G!GEU*N{2y)< z6(G7BAPaxfbPFWRZdl_omc@U(+37+=3FGZZqLjQ@{Gg4`Lm-8Xx5Mn|Y(-%rQfNs} zd82gQCX+qY=It)@*89_Rh3PMXxzj}0=x@U~maaE!G#eCFIZf`85(`!H*IdqJP12u3 z{#U*UYHCYH3Qqhrb1LzOZ5mz;?$e5^KGpog<}z_G1|3{WI`{17Mv0=^@nrXJ$>?L3 zI8ixp<_F)3-y;B3h9OCOqN6Z7b{(@A($Scg{j9v@ki)Z9gF886zDB~h$FP+t^emv( zb?JXO-eu=ex=LdHFdu3Tt2c9i3}a0ZmFpR?l^U-~QfEie8HgOxmU zo5LhJx2%h>xR;H_Nq*J%I(=fE*Z6@UIR7-v%Oj&bzPrvPc`oFl*(RE?@!(n{UlcAZ zkRHTCo|6!6^a9o!aroNrLj|MmyjU!z zaXX9~gTjiDVo^TzTq<7HS_k6zXCx+Yy<2+26Ag(H5zdJS~LYHMpdVlbl7gCd!S{K0nVbvk(?3_1T@7Fqzpi=IX$gkZIV zC+L!mCzn$uvvF}wj`C1gY36OieA9;A*DHovkK9=ph8P~W0+itWuBY>@0|cbh{VA9+ zAq#$D+g8pDJmnwkr1Zy{Y*dy~(${ZuwCjmB{$Ln9M3z$WwEp;RWtgrQ)uQ;a4@J8T}7-UvIk0FT=7e+6^z*vEz1%? z5x4?)-9eZlCpj`Zpvg-+5AhV#o#|w*TfJ0W7Cdk{`tGf#r_9!WmhN3l4NLRbYYR+V zC@O`d_ARwpWKy$*T+CNm`e8%+3ZZ(&0wr9wbUWDHIMm8o#Zw5*!)^J4e zZYW(4EDc{)R|2=(1yGeIm|Hn*QpJedc1-+##tB_kL#6w`A1DM(Kgb_9^`v_ZTXnmF z$A+b+v>~$LpnFGEyr8a`3@_$~Ci+fXJ&c3dXf*fncRhWCTV#g@`FVEjg0{zAyF&pF z`14drsFJ|sayzEg%zmzrVyLH-eC7HeR9@XM;v-wA<@gFHxc47a$<0QBNbmTPMT;BQ zip`gAwrjBXJ2{Pe$ZY*5Ii0zJU<*C2ei6@OQlPon?T&2UiFVXw<2kv3J7e+GC0kcW z!Wa!5$Aw|22TL7dge4VO9(-B-s(vmX)c)iEk&Yx<9=hntgGXZ~{bxPOSIgp&{b@$T;PV9vPD)4beolRb!y_P*#J*oJsKI`-H79-(<9t(kYg z(a}-9+a02^BG?z9n@2p~r7fSlSdW9tj4f)R$>Ww7b$$Nx^Lj-O7r2B2Z1Bt%H4NY+ zZE--D@}S~l=>2RT+nlRI?t@A9vvaR(C`6NhHf&gW#P`THqxhlR6M<9vOkv!wm(c#jkw35n_FpwPD*hEadMFtE(TlAOn)3p^r+k`_z zH6>DJ90#Y}CJ^e%^)GKO*3K3K+TbC;nZI!3r6-M><`|=u>wo}j$rv2#JmN`A5zOfh zN~kXBtKxOpp^?xQx8oc@@ac$h;FLoWSm2C560d`*g?CFF{XHmz9 zhljH+8#_7(JYj;|a5fM_T4#zklSCXZBuS;avZEd6u{kWb*juR-6gus;6Q}0xz7_m~ z?q}=0e9+OaRvs_4{Dn-?^?$W9+8Oo3!y2@ga*5R2t^ld)%uz3GJ!~@Y<}Cl|zgVx- z7!efo0we$NmQ#H2x^;pR@X%u1W}6AB1dPA|=lFN0HPhnbckk1P1g_XXif-1vutT~} zilE0WJ?IJb#rPi15?qk;QU7`%jDp2}j#N}wiu6<#&=I3WqjmIR#4neXp66^2c>f^* zb;kHm7B6j>(bini9~AW7P5-Y4=uP)#_xortm+7=cA*#Rbz3%NW%{W-Uc(E7S=_lUnfC7olhc8@;Xc`e54$YF}-P2!`xxarYr{w8b=7 zoH5enhBrEcl%nxuD8@HxqTXYk@;6*<-Ko7aus}q@`&Hh}&;>!@9A5^B5K6T8^sO!6 zhH}TWPFj$_A4Qlm*YNjh<+IYY#J){vTiR$gDC}x_?ewd*M<9%?uUg*!Da;J`hkkZ# zF8nCG<5Slr1iq8AyyPmC-pa-i0p3UV`xvBca1h#o_7D541vc4I=GARIwFvLR&{-E& zYlpZE0TGeE%|jO0N%tM$NmIc~)?V)Tg~12u1R5ISu}(uj@FlFOi0avCVe-$x?aum^ zQr6h9cjFDoKZ>5tN=$^7%VImVs}(d1mY%uiR_J6;83!|`)9kCxiURE5RQAAS z(hy@qv?wenKuYZLjW}bVgMB_y$xkFba>GX#A=b8n($+01co?0tFS^1L9clUFFVUAj zKKxUiw!!2mYdgl!!`_l{F-Pw+I5@TW@=4mP)u@G*YBPvI)(*~n8TF&pyt9}dG|0i@ z$AzKpx=w~lZyLm63De1?KIK#34srzt{E2L2>Lz*YHVP(h`Bf&$LE{>37)DSzNB^3T zZj{WQAOJ%Z_2m z|8>eYOnMcrp4|Zgt~RQp(vS6W`+OuLv#=227YoETwvw(@=AMwJ=Gad+Y3I4bt&>Aq z_*3w3N;fAK#u5sl6Y|eW|I2IrJWOfq+08CxYSKxcjJMZ<&2r07tuf36L>l=58rj!&s{}j% z|DvLFvQ!d~7a*rD2QGDf{b=o^eCJUtM^~BqhoFpo`Gp%WC#%D18q&320_=?<=q$F%O%nC+F6S(y9; zIS|4wy}wZ*3jnrXeb#T&yXkO>ouNU`52%Zt@e~R?TpH+*U7p1Ca6R2=C*3V$=V5xN zs5cDYOoY{FMq$|A*kn8@bdwD_%wkY~LPRfpvNqW1Q~2hFEx)=s4$Xw+@i(X;X$mNK zP{-7}*a{(JaiE)KC2EMn@q&_y8*MqsN}nRTn-Zr6 zfJePeQC_2PvPN9C`=eM|K=i*cg_Zy0rRPvRzq80@*Y9CnNV0x`;%auVyi}Ngd&|tnJ3TdKop7a4X;lO7L<&sc>jN$W=iz& zBfDxp3Ge#U1$Brzbd)n&RSy~DAGXC$6N%N?8(xD*l%0^u+!$y`$Z(q;WQVh}qju5} z5BJTj;k@Kr&4RVE*tsgrt^$~7+c0l)%C`bQCR2dJL#0<)ZW>1ia+AgmSD5tH^~V~Z zp8;ym;->KQ#4C6 z&TE$CS!uf|7$#Vefnz5yQBCPXEw`vqdHu*J+|$ui@fZEiL4Z;Qv*rzT>`cFWgP0>*z{1Re*#-mscWyi2OEg5W)@@KRO2cg=dMs?4^K%l|& zYYEjSWmmu*>hc&v36gwnuoT0DjcYUuL1*SE89%$^&@ZAo`ITgl8 z__CU@p~S@FY{Fqaig0NCLRe{`^}U{)=JQ`jA}%J^@K;vI(~^~uCSbBkpx>8>dh}Da zI%W;D=<}y+?8KaW!dYL}9Pwf?)T92$UMZg26;QPaeo9_l8{wGG>(=^(crB2`6%&(_ z#UOa|?rS>^&la>vexli=$k+1Du`WQ-H7Dg7J)e7%^J1^%tTxxW01j;j%p9HDjpu=X z#`InVp`7J)WBZvS`w>7gkTXP^#Ok=va7S1eg4nn0RBZ1Gd%a!w*g@A)RAyT8H52Ie zh$iHya3DxRI>C#h_PHWHTd}+!JO;SQv{MS7ppho=LN@1%P@B&>|3)CB`PUAB{0VM$ zwnGnfi`VI*u;`ytIRA2g^`QlSgL>te5{(g6U?pIZPu=>GJjOkorO>QyFi z>vbT3t&$T{4OAlw?w9LX{_xg1NFWD}X?dIy>n)I1JjjzEeu%O2b$Z0^baSWV;*@sN z;C@|v!55(*m{^rdw#gG$1SQcic8-c{I<)l5`|IO*ZERE6*!j*=K4tm#-O_Uoa66y% zRRJ9zUB~ip@()c)+`uDacKSdfa3|D@=c1b@a+5iX1y?!KKw9P z_xm(8$UjIVT&1^C-7W;oO!N#KY6SMoV$(!e@ErEb@J`g`yY0T(qp2P@JvU zi>QH29kv&su!?)tNY7rA8e_a9`h(I!fT)gJAz`y%D}$h5)={w5nF!)|)QXj`Glllu z7E1bh1ljAS&&4Rnsep%yC4BJ}ShxMcsf^jm?r7+(d=Y{YaBtA;mqfig#0kim=V@tn zRl3470N4VMwtri@4!Sp3J#PSyeRNNZ8Ru)u!`&RzvPv=9CKartsO5|S>Bx4N5G}bl z%7RMe?RTJRiBut?xd8FrPnJ3GIxP@}ZSH6LT}NXNXO>zC`7kHN9N4eRU;J>old-#v z!8{Qm#(yHP7%j|_RS?{}rl+R9+y0N3Q9G<~GdKo98{th*kV}(Uc+OCfSXxo$qbO2k zAdGt2rx|Skmt7q4Iq>KV!2k(Un&tom>++DrEY1kc6U$YJ)~O*O9$e737mrs!wr)#c zboDCD{s!4PMJNRcc;y7i<)7XXTQS&VKlzplc`FEfuR{ZypD1>hqe=;7Gh4d%okvr* zN(6L4^@b=$`VI4-WwIU&@=f218SCoB+?k@&kqKUM^q0%}j@8ARVA@MPDB%a!A}OW* z$lF9SDzZc*WetnUJF=XzDuZ5TC&JFtjp01$tL}4Bt5-i&@|p-?7t~8~v;jnpb}=5h z(Gy59%0;c7vWTM0X`~oszj>quP&QO$d5^Rnukeh=v%`D$J`IlMpt*kD9kb7o(&Ad; z20CqP-SP8H_Wi1gm-J#LzVSM?K-P;o7Z;+*Wb$m^pFcj9iRfMT*lfI_rsk&budmEh zjhtJ}?zbx~=0`cV9iT(T`ra)f9%+`X6;YJWCA-n8=dCwjjp?YYk!SyR&9bW_Mg4KJ zFzG^1k6`oR(KD~{@{A^lLSP_xp33rGGvtC!U{Z`BK-&Tr`Mn!LKgex!YJTsge1PMG z3wjVG^aaGWdmMJPAe{|qM~#UoF-MTRCc>h+{6wM>XmHm1Zv91aj+i%;y#CTksQDdF ztM`8*E(u?RUK_bwvz{EX$Qvq}o1|76f=)VfQn~`P<0LAk5Ck$4;Nnqf&cbp?l6Y1r zb9V?fAanP8V0FMR(Ja)RrO5vUr?*v#aP_4YVbopSLc#ahG$LToNDnNJW1TE#g zo-*famJV(D-(}%cAG{%n%xOlwbbiym!Cps4qEBRHGvS6lg8`n|d?H3|Yo8{sP?24T z_JWnRY2>vMcrXhzRH+m%YEw8-lb2 z&9S);7UF~lr>|+>c%^FXUss_`^yP# z{b?kkIzN+tn{I?0Z^SqeRiO9l3t*i0*(H!1}`HZo-aFB zVuN^3*pfl&>X+JVtU zmq~XOFdRwh;S1bc^4!zN0B(n6F&c~);)TDdVSY*hM7*yqjZYt5-f0(EKA84?=$Twz zIvHBB$?0149A)DMr!GX)Y9F#s`A;;uBu$=Jv;6^bXx*9-2#<;#GR>d@7EVkYU5S)< z9p^FInLpNIsvh+c!p!mMG_1|s+kY}tC5$If(7Ry{U32$0+lax9+{ zH}>|T`P6#sEASe-*Dzn22SyaNAYo7mhAegdH3GA6$8ee8#R1JDEE&Rql$rLdnk{vO zcxP0Y^yX)GyOD+m$2>}t?iBZ-jAtio^iuTFV-f7m@zPeh9ZO3urZ{1REX}VGNzEZe z{lJ?ajsc5rjkgTC;IAGo>cNIkBpWz2T@_cDe7+TWfk)h?`eD<7am*$=h|d>sh`t2( zbGNx8x<_=gx_~%++meAa^01)bo3WWU< z+V5N3_o_ezeZBl1-@eg+2!MvrN+}=)nM--WoQ|^h`gXA02!l1;lPdSP^?WaDYK08N zZ*mw<_X8h`=FSF09e&wMH9}BSz>&gY3Ke$)k&>#!nuES;*^6UV*T=ictK@*g z~{ly8VH2?iy|Md&aVW_!SKn_24KVf=+^nG3yA`Y`> zpUuRJY$D3d21v!>@c4A2`iZ;FboA72C^*^3dH&guUGLEqnS7QNvslLq`wv?)yS%Hjs@`!oV?cWh~^(BjEi!hnjNHprpghM!tU1~PQdVZ?yc(jb9 zI}7M(wpROb)u1J>U93CbBb}O?@^s*8odd2)3k;HjRFt9(%}$WA@+}8wZ_$D| ze`7>E^oW?AVQLx!0qWJ&uwC>x?lN>q;)qb9lHqjxFS<0hZYdIB>`!HtiK@}GK^Qs! z?iSEBZ7`3uyY;t!VNYC<3+NWWh^OLNYnwAXH?$~?l;f^Q3 z9cQO!$}y5%#r^-8ywxw`MobNCP4h5I$p>n=dCPFGWbkf)QXrV~At(zMY|bbj+^gQD zNsZFh8TG~#OZkA28~eyB1FItfb&&2=vpa$I8%0&<*oBYi7@!m5Yn#8*YoSjDQfzfV zom$2pol=85A!&t+-cZ2$DAqj)gvl>LD%J$gqP`h|<{ditZgl06Y}N13=eNQfg`nxAE118pvb(ZG!ujP8jI1A8NV+5=6B#{sj7tjpa%O2Xd2Yc~{dQBbv`6C-w z;{>R@nO(VQUTpFCnPlMfZJkZUY-%_df{xug?*E;dE-u8NZ@`$NmLsMTz^r(vp&QJP zPf?iSbhqhbby{QIN=T|~u)@R{gX3muNm5sQ);??;!OOJHfr#QE_y|O?t^gt7@=#8* z(fw(?mwz)F#)XtQ00$mr?jE;wfN-s%oGi%oA(mGJ1tMhYx+A8Ljx*yQ_3rP=kff+)>GJso}cGh^p(J?$=4w{sx;+O zxYD+^_C)8O#}`dS#Nb}7evCgwWgc-7XcWU8? z8nWN;P%s8-Izz$J7wS=Vt|kz35hB}vZg$sE8PiYHyO5)l?UkSxi0~^Ae~obIRSb2= z$H%7<*EB9K>!g|?jQvj0%TH%lBkw>{aVFUsouGJY=>z*RWe8+xLNb;O6qYrr9Q79c z$ZoAu1bO>vAgqtC*)Ax#FoM~gdSenA zXGGuV%)D%-D{%SsxdSQ92Z7nZZU-V9zH%!6e^1+VVy*&f9U)!^hKi|2bONVXl3X-oQ&_o&Nz-%VU^`vC|WEG10UJ6%iib&y90AOjk6>i96wsAaVBSE~9 z0ai-%jHRYb0N|LPv!buU(nVG(z=#C{lQF?^Uh|DW*BehS?RqAOJ<&P`{qZ9fHIX-{ zN>TUL%kE#wSaBB+m!?qkE?>YL?3D-tAi$yDndP4h=nU1CSwIf1+uAi0V; zt)35F0av;iH3AxDF(Ax*5%RIiGb|cuvkH;6Ef{#MQ{ZUXP_Z%RZ!k6ODpY7l+W;6# zdUrH7%pl`Q(A}Hw@TA0#NIYL4b-$yqinE0(M&4Q=s%F&$J#k1@(+~p|bfd)k-N?J- zv>;!*q73Nv!)!R6WXUCETnkDWCwNZ435qeuR~tf%NP)}Y(ZmeDo$V*plnjTVp+@Xq-fS6weroy z_@8WY-@~j)F!Z}xB?k;_@e`FCY?ihBoII_5hd!@PUeE)1CTBC#!Pf<8!k6Zf0aom% ztUyw$3ulZs#sKAfSaE^Pr@V$QE3javkynP4@8yZ5yhJ^%igbJ3)7MSC8smCqvnKN+nROEx+%D)%EGYrnADDNAL7} z0N?r2pJ+LkzClXFCk}r3Xp!sp9DA2IcdzN>{J~Ws`@fk(cnOf?sVAToV|^2mplh+U zM!a_TV|}bXnI)hB;UF6sUnn!0GFiZ)M$O!71z~g`koP8PAWMy&;<(%QAnzY1$q-Kb zjoPso-MvU-##y8F^a^n;y|)0;qq|ImUf$vT@7bd-1Q*|honuGn85Wmfb2 z`$zuIXaBwN%)fVjdVYe6iN~^X)OP$hXVColIRLd%)JUq>llH=zY1g?QHlMDAj=MB5 zMR%i2`|VRnflAUK#S@e=KiANgdr9*))d5^t&%HQm8Q~Ge@p|0ctiiz#J8ZqS-PK}| z3eMiit)Fu{XC*xk(2p}`tv%aLNxh?V#_d-_B?2Y4ow}jWPC0h%9hW@y# zbD#3Xr>K^bcjwKfuJUmbyxO{>b@0;2f&t&O4=)vkXL#FKnqjd6fi?Es*DDof7L}A7 zq__O@RGT;!P*s)ujA1%U>-;uD!y^{tqLypH3Yj)0H-wSpaIHRl+7^apAeGa{f@{sC ze8C&RlCVJz(dFsG6q2bL5!uc^H^Qj*-IfmemN|MC)0)*0>5dIumy`Huc;yZ{VSq_p zfx@Ot;BJ8hURCZj5aM@(Nme*c(;fbl1jNG%RSUtd9&m{9*UEbS>X>5~$&Yo!W}$l%Dlwb#+J6a;@p2=slT=_WzS-vKNL&|S z9jFpN1*&Rs{Bl`QLezCDU1vXS%(zNiXIfa;9VkP7_R6U@RD0jRB6$Q{>ndbs?FsL=olqib`k^tpB{ z>)G@@k<$r!)wzGrVLphu9S2|=j}VCBNA)RcM7jQLqYqMl-1ubLXCv2+#}WurDsUUI z6H*o^7(^vpG)&yz74q+|A)4l-+p_>0;uB+{;VT2glOO{c4fNN`Z7+b}#B%B6&9Z%S zC3&28TA?A-N{yFX-fls#;fuJh8vX$^3&AWe=I%z>=xzk6L~`b?THo6Xp|G-P}(Ll4*Cm43WUtEwSkq)Vu~4-Kc!$qhybPHZAy7 zwGGZFEu7pxG!y`RNE^EA0XC?G{^NK`1~`m;GT*Wil>)8E3uJ3X99Xs+d)N;Zv6!&< zXm~TZ7N6ak?&j1!ADXs@HXUuR5`&vb02n(_muL5lLrQd86h!m zQ8Z^0$D28N<5@7>*UVqQj8r!Xp1@+j#*A;Me{8er|ML+j3jMJy`EE{dF91%z-tgO+ zy_VB6GnJ4bJjvd*%P;Q1Mvdb;b<++Z;Y|gnMS49WnV2tGu-~r%jcBG1Ymm@KzeGUC zb*3ZZYff6A)?MF|)*r`>XMZ0$;b0EC@d;fJn2-vmjakQ}Q*}f3Y4O{)ERe%rA3Jvf ztrD>oTg70Ul~rg=WL4Ehq7o#*ZMKV3i3n!#ktk9Kl4cBZvH!*4out)mpT2%I833KF z9_m7RS7YXcEXqzA8xz;n%F$9>n9nWtS*}~Zz9LlH==rN@cbK7a27$wDY^~sIAPmq3 z1k;M}ispL4Lfg@6n=Vq{7Ui zC)4x@DN$!KmId4W;VnwPV4||)c>rc^ZTMlVNSbVfa4v3La!l8ybGFy)z3 z=|d1|4}puVdB@2G%N0UebZe%tU4R%2xk(YOI2S3HNbzvUB+pONy4$SM&J+C~9PfK( zicIP_8)5dQnRZqd1T5s&oJww4sXDb-jQ5zdk* zJ`T@umQ5@JLVN?_o8Y^==EJf_eSNxKJJg;hMoc^jFSAwM{yas-AmO_gYzHBf!AOvC zd##E67=sot#CqZkGj>Tt9TVGP)eNy5Zpr3a_sNK?X-ngkp0Gy!JU1r;$Ds^e8UChQ zkxnee*$4!^M3nQocJ2B0_EYt$qO?#;Uma8V7 z2H6#31w@+_nwg5i)W^5nEw&AhvG153U%(S~y7A`Up&`A-DdQu=fTI=bubw5}INB z-Wm~)+nGksp~EG=I99rpy|B)4;Fxt1T$xK9b32|ItuqD&MDSRWe8M9!M)ETWlGQ{D z#}!hxSO#Ne6QJ68?N3rLWiS^7?8MzlpF5ydR+4+Tt~vZwqm<|TWoCQpa^&--J2bwf zRv>NqTxJV!5+OqmXualZEqoKpKI%|cp!1gKHqBajsqJs4i7CQvl*PW|{Wf;$Z3f`j z)&63(*7N<3qS@80t-YVZMc-mvCwi(#i&B?|B>dvgCx(YBKaIuXutOR7j>MFf^jlYR z*VVH82@md3rcc(NFE4WU;bH1@kPi{C%a|rwK04&uIYLexkadJTzIo}x_ zHFA~okbMfhqv*DJozX~yl2WR|!jLsXEoV!XgUv&eMXmp>ojg+bE-}N#%O@k=6xJu-KnyM-sYAWY- zP8ou=);E$0s|r6{jtq3>Tc%W>+!EYwugWf|&fHdUylf6t_L1Il$r_@?6gww{0WBQm z7gn4w*Ec(7yr>MRIPfIME|b+&h3a949>VBzNlP$I(6mmL`xE#B1A#w+!8c4^%AKRW zhV^vA%MxpSGs zd_znixlvP-DE#B4NW{8J8kGjLbwFY<5I8c>C^ILAPQ=?v+k8cW)Gez0DmH4~Al>c< zbB$UhS_&Mda2SK_2K4SQRGs$~i_^!;G8u2;Gi~<+p_=x`8g_}52cZDr4M-=AKWE)? zN7uF4(0()_d>?JweDus|r^4YU(<*oQIqD8N@m9`tr1a2$S9%|oigGmder$KU)aM!N zFYWRdEGC@f=!MMi@&Jcg2p3aAD-@bz1g6a>K0fqoWPhV8LlP@wifKRMnwOR)878hD zmZHtC9C`cwk5z65Qj7_TTB!!QZs3+2I`I~=aMUh&^K(ZnukA&kp_nin{YaDqtDyMxY}J&6UHzk$iN6QRQ||wBE_};W&Y%Q-jM0_Zk2M^Kd+bD zLkr}uL339>X^UlyC*UYkxAx>GgO&4URH^MFp>_!4P8~Sa!u^KmQ%M&l!{OOhBtu zeQxN#XJd8SU);Ml9s@*u`FRq>oRq!*GGr;j#3nw^9finC#T~(G5sy528dgEqZeE}K z3y`1Gh{9(fnmIE?mS3zk(nlti$A8%0(xh#6OFi+76?-xkm1fZ@K2$m zJP-&+yv>5sf$+N{UUqEM-B^BXGRA^7I@V6D7=k9ZY(YV$jWdaLH_CSL$fPLtEP9-@ICa!JX?yN&xr>v@4G|gPdiV`-&LwSF@bluqq zB=K}~a@uS2Ivvz_I`HoYkc-`B0V6FM6c=VV+N&pj;x07jY!~L6y9vd!<<^Xv@orND zQej9G-XQT{!7eD2Q)AgyT>*|xVSidQ@Ma zqkqHUuC+Gpf|Hsj_G*Dx8}1i)&3EsOVR_^#o<7a~0G!!($xSDwk-M_gyXSR@4s9^r zqa_McLnI)G{LP#DbrFxc^(T5B!G?n=;fq($c~M&!;>UntLnt~l@_gkCY_%Wn!9+A| zX0LId@CAPR29U`Yo{NdLTebL@eabn}h(`_ED6=XlFtS2-^Z}pOVO^^|cO;f^P#za) zqWrAMQDUk%QgjqnQP6P1;Y=t=KZ*rP&{@&zgw8{fk?6KDnkl8 z>%2ydR_PLCP9}GqRdUmz9IV|w*%ck3`6Q*e8*ZIX#h7)Z-|l?>3J)H#WiU(eQTFpo z?3f1a<sw-t7 zbJ`y`o`A%bl`-g_a{tAwTLC>XIXb|q!La!mG@qzMjld9zg8eifaJ(slX|3Veu3ZmB zRW9~g~`Cq6i+!+0w02LAEE?;kKj96Jq%k~*7!L7jb*8#<8O zjmIQL3E=*RswC4pE9rHF9(h7aUxcZ%0%{Z>u*%bOJVdJIL%~bHqPhZ#i&KOlU?ECF z%gK^ed;zf4du(23124>FqDqtv8C_DMpxf3w*<9&p%XNYaYoVJ7pI;yAt1uJL3LV9! zBc>3;6B}>ksBxm;!(GvcM_UKRIFr1=B4!&5X`%&Q0nlh+6!lSu|B2%8WTC6swOGV; z0ZG3Dt%#6**CBp{gzjyv^J7<5D6mY9w#=4o9FID+zN+A5fByq17?uF+`^=Gk8qMGA zQ`lbGCglyHk+*-(G+K`&Di%;vCU_y2nG>Dk;o*+mx+AlBSpSGxIZ1tjLwi2EuywC- zfJIw;5B(3vLb@LZ-gUZZ?rqsE)K?*pg8$0#ZsD4?Zt;WGR5CjSZyddi-ew(P0OZ&$ z76Hi`0OX)kAHF!LWI-!^y<*h9%~V`CE%k1$Hql}5s&;w4p>tKiwcEE3rxpzU^{V<5 zpTbeZ`&;_5K60=FA(4FsqSC?_h(&cjO(1bL`7WlxVw?7t0Xn!r7HMIceiv z4e$u}Hk(xShF9*+qeR1HhPjr?X>XxS~R9iPOo42`Jr?}5{rU(ZuBx3y( zj2636gm>*?69&L=Gt&qL5rWnIZe7W6F#B*bZFqGS3=uBg+Fc$6Y@h@V9N{<~ea}nG zbpd%h!g_i+uJz^((pa3P=Rtauf*eFMZLpR`o&q$Iu^%2oKjoO&)0t$LuXlZrW)z+x z(|~@Oc&u@M6c_J_8nJAQrq7^5-CE;3kGW4^MOYjLy%r!j{?ltIXg#~rHbI^?u%qm^4xv+)fj%)HQE>i8M3n@`JV-NtRK$8@Cx2=W0K40OupDZkx zjPvgbgV0Cwc?gr$vLuU-(|jm}Oc-Lqn4hk4gHdb2FmGHIHXY%B`K%LG8$A~-|8~lU z4jcs?1BJs=n%767i}_rn?i>h=9%zgiKgMy=C289|k0J%1bc#`w zO(`##o_l;YYV>p0(XTR44_G^ph_Z*{zIIyuzl)WxK-=TIU;*4`?; z2uN13tdEb+|6}aCs@TOA5K%!;(9lGB9nuh~ z3f3T?Vu{kE4ZSE$P(VP2K1h+`&|&Dz{LUTJd1vsu`T5uGCy8+1d+&3f=RD_}$Df1X z)^ush%$>{^k3KZ|PW%uRSS}d+#9MZIv5R3;+TA2eL#Ein6Aq`n>uoB+C0#8> z`rcY%%Jp)yB6yF!t`$R1cQ-NVTgIb5NZe38^tmA3(srD*b`BHdx)`jthB0tG!+SNp zJMvx7@Op3kHnSl4Glm9b;^;42XH`)Iq*zl5JLH4z+1Zam!e%`Z0;?95RM z-N2Dk=k``wbk&-=*vkuNXPtb+KP)tC^-QO3CtT;9Q?GN%0%-DQyj=`{A3 z@;h8hC<2Ad9dYl#6F?hw7c-E1Eg^fj&UQD`L}qGVZ-)t=p0aqTdUbNHk5g_%5`~EM zZ3gc|qFm%?SOy=iyTO_0-ahopbfX%YE;Lh9`uEj2b@TNW5j=+}rreog@iJ+kG+dGk zA-pH?rK2RLEpgWEr362_b}yi@NVKKCS){+`?xz*IK;5-T((sM1a*SCe$3wdDe*Lif zbOpJ)Aa#~Xa(x8XL++b8>#n4dg$s9;e!po`W6w~A`p&S$!f&?EF9b_b1!BnAL`~rO z90!7(47iUTu1L8){AkgTCU1^YTE}TTl&H3*0ROiat2h>`@}VB4SWr`LPGEPsGTU9< z7KlPN;V5$;hMt5;v;ISBF5*~qL($TpJ?QU_xAU=~2JyVck^WeYOh>xY%bN=8Hs$#i zGCpJc@rfpY#x=q#Nr$DjPIo4NAkwTjm1f&SEv=sX_{HexE{lOZA?Z3%Qy+8blF_^+ zUL++K3)4DX(~}CbwJqGys94TP6SU*65u$}G1*w!PH9MBDcC9_Df9W^1HYuyN-T%gz zWrv5}8d+5yfG09Db62$r<_N;QOZn`E1d^$RYK1LNF;@E9G;Q;d9i zGQ{1x-YmgXQeXG1d|(n~*g@tKtA2;poLBB*=w{wRH&aV_*VJ5P(-ujE+{YLs%7+(p zqO2&H`L}*8Z}bV>Eyr-EHoz>aYlBhtOZv$CR@DWUKXu2Y>oO4cNZ_%_dZc}9R+C-b zk7V1+_A2C{p&7tQPEIz+^leagviSsacDVUaR2u4A1 za)V!lAU*wvn%3a-hZ01DpPss|!-k8zscs_7q~`dRr_&8tx0J}U!w}0G&r;9+EXvh} zp=N5bY6%n5^{8Gi(s^t2xaT(p+b<<(lFLe+u;aoP{Lxhx7l7-#mfHqzvWYzrIHk#} z;ZG*{CBEIQSHSEBKc!{uk*{lp27uZXYkg|*hbs`sdta?{y=;@E1_IXssnPvRG0nFG zH|4M^WY|7Z>T14fqitY~+=b?>9jU3(PMM{Je^-&nd73#lUQDze?6B`^xDacmNccIS z>Z2v5E-SZuq9m!DC$HAKb-ZGJ`laxMR*VlezaSMHUJ)srs=el;EJ1^H3XhRh=tS92 zk9)*)0V2hSkMPThYOQhjq08>L5fiOV(p9M zhDYHa5>yA+AZ!DWRN8V5e09_VH{`xsvFFBq$JQaCvSf40U(HJdVVXwUyd=VC!&-V- zVB_T)b1+m&;VkhYowG*#?CnnAHHhkUf`47^U@go6^MFC^Wh(dGvht6w7H(c&JIW{L z04h4 z7J>ieFy3dulkW;tSf@*Oj+ke)E=6`Suop}P9HgJVaTl{JYgGgPM|rgE#t)?xKfqTLg%-Y9g!$%DdF>NVnf|f`)rsrV z>uMxRem-5Z|M2&gEviFX(bs2)1;&7MmDtIs@t21}0kdb9pS4mvlHuwSek>_w0D})! zr}bVC*=Y%vdRywUza_Ho#D>`+&zVtRwpc;gk}Ko-=AZXUclErvT{0{uFo33M1amPH zA)dfGi;>=-BA>G8ZwUM3F^svo2L=Ys2Tcy4SK=-xeVy~lAyQ7D^dco+`w0Y5#c`;vWY@WwNh#!*vnlWU<`LIk?oi$|^@+0#?o6ky1u}Z) zcOK63&PU1+{|S&qVwJ0$m61xc^;sr@ZIgQL3HqS1j)G|cl2k(|M|TFwfFCnw+vPZ= zW3qb(?4t2$z_I6ojOAa4EqBVpqex~5Uft^%7eh4fhp81`Vy zw)2&evYIdJfwtiCnlDyv`BeP=XNAjfR_~ju+K6Hm7^9l!JP?ZI?m}ux3K6q?f%0om z^bBF2?~!toq-oxZl^I}E<5p98th9Uk^Hr&5+ijyf2H^DXQ&YmohhVcTE?%ug{dV;0 z5#j`Pp29qHj{mceT!kIXnzQvThP*?Xdzq4A!r?ocAHE3;0#10~#3=gw!ceB%BGmye zz3gY0uUzkT*Ms2RwhpZRJ~KD}86;a1A=bL5IJFl05#}vZZ1!O7hJ7QwezE5Eo9w{- zkkw09(ZK}o?A|=7kZ^@XXl$4AeWTNQ^8hUU4PicXGa6MPw=6YOoL{T~e|l5%C3_-8 zX!vD@_doc&4PkPXzDb=^`+b6`<9dT^sBu43TE}7|6)2@ue!~ZrK&>(_VFytlh?RMd zm+v;YN-no%GSN=;Cmls?q%q_(wb|O+ebnT$w;Bpco~q81ugnbBtjHfd|`o zDu8++Al{?`Dwi-mP+lrYfBC<&v@6}(ydH7OZa>#65$fg%bp}PJuWxG|tTC>kt(sbh zode#}=zZnIk3s79_6b6rSL zqbpT_*&b8g>#XuuxjwxSXUmY6pG?_#byw<0S4t?#-+FiBb0E%>g;F!rlG6LT17IF} z)W1mTywE}Kd?rMnc9XXo8p@E$;O5a^GP@-ZG$K4()shRnXZ7xD}^V3)yg~ z&Jw>Q)wmHLimam?6r^Y+=O1f?f|*lf~cqwU_x97d3`TP;TOIA zmUH`k^6Bu6?|h=*b`OOB%yhZ$U-MQxNYTr0zG|tsgPzhj1WvDTu^l+cyILIG^ z%W)6mS77i;L3y&PdpsH~-oH^t?Dx&PzShS9p4h*ADOKH@=R4YSp(ztks;<;|KF5#> zwYW93J%z(3c<#8_eO-3jD-td99Ya3>w^I}o!)J%p|UPAiZjFE?Q zB<6tST8cuXK#3?|e0K0tsQC!aetVT4*Fr->DLswMb1}`BNL7-j9&D3^wnXFWkO^6~dm*uC%X{@|f6NI; z5f?f4JVpIkPK+R6G5j885?!(=k(vQGe6G4nf1qJbp@MNu4`4*k>nOwz9Cmj3qXfkq zc=N*)EHYEBKZPCXMYJ>dj?>ec785ZEHeCoX%xsxV&5^Ts)eG%)Y{KX-(;H(j+2v0k z4TjjdQB;)~2-uwO;x&GyTL>fNndhQMpH6S`*NKfWZ>uy*= z#}ZUrWA>JrpGr#?%@7mINqDf&XU93c&J}aFrH;TDoFB*w<3w{)%yhX9NyGQ+&z9aR zRn}bow+PEC4Oge$oZhgEu@ut@5U&i1QXQf(0T#%t2Ya^ED|=qTJ$u{hBYS8)RAns1KR{Jak^>Q>`i9-Yrof_K%AH97{u zkg+IX`iYq=VXo#cFg{h+V+=CNYu`=Zv%ip-iPrfPhP;~j@Niy0{P!4|!{rX4y4RWt zt*oplQ10GS+TPV!*EmF#uj;dpLje+P|MXY%ULa1hYAN)3XIlecCFxV0FZ1hgy$N@C z1Fnj%BBl~s40eP|%5VQfW(s}SHi9D(ijIZpuD@?^+YbgGVaT=wIiH^qse9cli}dQt zSi`xdE)x5uN}dw!hZ!{nz2hVKj%EAQRc+zqiW6QS{B-dTH%X|JyW%V6Wvt7g{9`xU z*LF`ow=6SO4~)>Oa~->}0Yq^Kr%WZkefWtQdo#L-=2$m&;I_ zp}=qZQPlx*$7B(G0^$J~keJnvo!8=4n=|t?glUTUPCz8`xsNwHz z0HJtUkZAN4Vd_X9!I23Gaxjw3Xnz2b6x#Ytb1@s}kLhbNdC>fbbb@9~Uk$jLM1g$A zLz>EKR$!h?4C~A%^bqIy%*!^M^T$Kf?tuP_aBerx_eD#Q57eW8t_BUIye9NGk8!+a zegb6p_5w*KcAx0=43{K=LI_{{ZL+Q<_U1|&a^98aJ06Ds7VS>ZXY~0h7;w+ww*7FR z=)pZOc7(gnG|@6FBB3oH#5*NpD9v;tI;G(9`&r)ewtV-63U*sz5`AE+^!7JzCaU}lK z&RR}4FE~7*9@SHE98}t&LDa5Z<^~;JrC~HiSMZtCx}5YWHRpZ?x-i!>+V}Q8?@T}^ zo!xLDF9f+lk@0^&MLXR5b3Meg63%`3;gUv}y0NF$I3!$>k!^#~I_!!lF!t45oyl|u zkh-5DZ9yHH`W{ZQ{`V>zUVxoI$G9GBHFfHcIY82j<|E@ zR9dLoc*ChRrBm2<`-9O9T1KQk5so3$ZxYx#>gT*{600M%8?@PNijj@;O0xrSrxNNAZxcv(8lz){d};&t^4(!;E6Y=&7_cy^(?&)+86a zrzEP*zOBLs4>y5Wu^2}&jKvnpR%~05hZ2H}ZpJ2C)0asdF%o)Vc*L^yega^*cRuT( z+FY+z5UNiWcrUQ5#T$UTojm$3%#hDAMcpxTQ`>FaZG%Z3!^_seLx2~A2{$wmj5xDJ z@>qsiOJR_tAFs;}d4o&o&R+iSB&G4|C&<)&N$s^(p;c#qDRD0QEi>s=rzRn^;QUDv zb=iY$(N~TAfEx-kF*obHQ>gt~9YR#ZaM!1wN3-u-Ag5}t^wY&}gwxT@{}w-C^V2bU zkrF~UFn0USA1hAPJ-H8g#Jq%hSEo{BqZ>kFt`3PMAcTXY)KM9|-m)iLHP4%6xF>Oq9Vml&I+S;FY9?Oi_X1!R5dDhjNkba!IBCF(c84_ zy(nW;_X;JP@!Au&Y_ga~63?A1Wd#Fx9P7@Ym$vL6&I5$N&@r@%Dx0La*&YzV&kC)8 zea^On9Vygh!NDYHu-<#)lG!xxIa8kLOD`bn<^fo~hzm!FE0!$1?)UhCl zc1p%DUheUALOJ-uehAU?P+m+%@re`Ef66_~$Ld@iRh-!o1gjf(WnYz${#JH{>3_+U zE~%>0p=h&j@2UTD(~m^mT6eA#$d58MmMNx-2Zo680Sudg1qq&Q04RazaV|^RodgLG z%YMZw?k&NuO}q48k#2r43yzZW)LpYAmmZBUMf9$q`w-SFvWBxqzvQNA2~l)5I}(t0 zJLCc+g7;kpZ%fBdbkp%<`&&CYI!$i4a=(D`=e;pwJSD@+313#1UTdC`bL+)gucFR` zV#a6gDWAwmSH3v?@YXkBkg~DeNr`vE@!cataUW7)YK0%U9IpN7aO3Z@_kqZL_5)qs z-#U2T-!iiFC3MGnvMk#>s!EQe$k*U6A7k0GbXoop;?f5`(G?ip!q$%P4Uv?DxfjBl@k zp*4mV7Tc~&>)#{N1rnyD>EBpb`{cWpjCViv4*w<&T?~QYH^7?~`~HZ~5QX!*o(u5N zxo+6>=Z|RXqXSt~Xmmx0D=vq$bh6H#&?M=@W|8{(&ZaXV8MxvW)@!eGsaId#Olf(W)Jqix#_-<^GUnw_>H##Dphp!2gYbrcRKCpU(Vb>sau;kWcNxTEhGr+m27-?Z|{ckox)MeJly zfc-t!R&GWe#@B6wx4hOGGm^nVHX->v#Bh zKIJUa^be8fPIkQ=&5q#mF=}(br)Af>1y~% z^!mUyUh7w7V}(a68@+Wcd?KR_`_1g<5fvC|af0^y^V?U)8Vf1yfSj+4h)q=E-*`Kt=^b~UH;Xr1IF2M@3CFl*|LAV*GR0^Pc zlB)H0H(bI2vl<%JF)G&MpW+&oS8jqeN9cZ4qpxQ2eF#L9U|YefoA`NB6!hGx=pqp> zFVS2>g%oJ|rxv!ccN3JwMXQ^7%jK#k-bwM+cO?xs!Z->SSLyokdB>Y_k)cYL@Gm;# zi&Ozq&9m`Cnm&1$+7d1~)3H_Au$rwGVv`m75jSOFh~_ub7hCU-VeP<#2T&PqDXwSK zyN;DA^N;)gdepdi0U-2UFA83a4SLRKAUZ~Su5SIG5#pUob41N#Z#zp?MEV34sVeAL z5bDh|e6mxj@`b{OeKM>AKdDTA*s9~AF@>30xzG+nYkJxA;R}->UKg^7nTH;bEb%A) ziw;&iMuK5RiW(|3WcWe!~$wlEVR5Y(84!_I}(CMPYj4}w#t3a!zK0a zO`0NO z5zPEx5`UD)D3)X(Rsd}QtZ(GP8$0kw3stx+pUfc~-`q+f&`=<=I0C~JP7c)>R<}Qt zVfPN;qEi&p8V~qQZy4*Svx}+D`*I6a4@BjqR>c~gtHaOpRcXDY*u#k`W?r})`H9XD zgIT?O$weMS5^VQf#>S~O`TpUO!4U*M_0Oie!#S!1?8iGFnM%f1BAKd4maZR{0&|?Z z-GTKlCzWV?4+zUVQ_0(PpXhS$kE|(HS0_FiI^8|u&fh*Z%tcTez*60Kerd`+u_@u* zJf4zqQGW1d{r^6nF}C8_Klf{$J|LGk``0-UtM48*VQa~*O^jQbSkBa<=Wy%3{?GpZ z{OdnvW(J#Y$6Wl!g2BZ>UoPA7%{Slp-@0kVRT%9#V6yGI4HY(pw;a?g26Izc+Xk4e z$MinlSnefI;8a{&gYuPwR8_OMl1g179{-se%I)7hI+CdW9~FRnrddnAnwLEbcx1F% z+;4nK%dm3lTslTJAPZ**GVcDk@vwECuItE)8**kxOT@C^AxsDqI)U&*u&}nXGdA=6 z$;HJbUlPn2)Pw=4{d*jQi-H4dga`%tDo$@OQ8)yG>}c0`Ew2$Mu{OX}z4D7Q+c8+l zAuDh1$92(moITf+o7ig=@`4%ogOlw`RnBWuRqjSRiq}P^R7?wZ%rm2v7$tResZodOQ#=^~nu$ zTZ0sqS~OpaGp&lT?}*Ne;UKXg*34CohR5l2dY=Xww1g4n(0a4ah)SdAd}cB(SO$&u z$BbH6r}3U*p32b$YQOea`;zQWMylOyR%_D=!N2Hjli)JJj&>dCGfn%AH)s*6=5U^| zgnraFrS0c(W2z}xxK5I(&b1=OSjUErj0Q?4PC_#hRu75nl=?NykeE7K%V&4Dtv^{E zXCQg|I8x$fNS>*dqdle*Jun>n31VF9EH1PW9b=fCSSNQ3DRDH&U*Uy($JQ#N!0^Io ztbQ9=_C7dU_T|Pl6bf8ah_sST^&@lphy08g*{3mkqs4~pX=cxY3B~mBw|eu~Ta*!8 z@+H+MgznAX{}w2p;}bGr%3w8{CHPF#CHQTca)@biF<&@31t=JG#O&U4L(Qdk8@FT4%=?ejjz9&x7K@sa_3>wX>kFcIOD-Sff?g(6WMpYJk2Y)Z+>g4 zv?^Mad~z9)O-Dm2ZJI_5J}?C9YJ>ta;&_TY={TpBm6u;l?KN$--vGytWfp?1qiv2K zp!`3YTd&cX{+1&H#`Jlbcr~KMl% z;Tpkgx@k#e42^#`EiEly5;t7B&+dFJ#Wdl{U}co+Ty@tMx}N6t?G+hIj>D&=MC{dC zB$Q;&2@{=SGuyL=bDZvSY=kv5uTIXP#;j?t0_%u%){e?2C$}7f<-Shf_C=Ok_rQzJ z#7dUk-CbPT{_b5tsl$Wwse~{?4HUH7M;)oFp1g1mA)y#=F;-vAIN1VDr6nm0OyY3@ z>rgc`i%BU6(4lYtxKkL|M8+}bT9m%LX_=|VvtcYLKK6+d?~t0H;1C-7(0D7?H$`yr zZr7%!E_~Z6EJ260o;0V?$n{jxE%1|)wZ^_9W22QwOVf5S;l9%zmdTb2ZX<#1l2u927maI)&uT}44(YMc7v)E&NG3B3bRwn;&t$M@nM}`9H=fI5tV@uEmJ033H zc?Rtk2SQ1w98#6pq%4TXAa>@f{UwVnQrjO)#&u~lH-EGQ#rw1wr2gxC(UyrI$S4~a5MsDfBDKRZ_0cA?S-G)1)miMp(*D#Uayp8`Pqka+1i^n8E`TGP2Q>V zC(~)(^GS3MJ<92)%u&%`?hl=%;5=GZY=md|<=Q=>vOA5}J z)K-;kSW3@3E{L|TB5};en>#K1-YV4YH{H@&o7DRlrgM)MFV2zQtjr%h0LM*=k?)(E ziafVz2K}=&=hXF-!kfZK>fGiMG3nF5ZV3Z6LIEzuinsT2tKQ=MxSNro?mz}1Iy6<` z6Xk;)BNB&GOBYeI$M%y-!)FCk;G3GDB5 zyGMYl@2UMc3`c;v$>a#Mb?#*WAkCI{>Q-G!e<8GbKBiIh)jC;bh>D5UttV#=AULMs zJ=NyB_zItZ@m1<-Bh9iFEbe+MOzawT8msX#msmw;22fm{HY*BLD3(ksE`1D>`?3dn z^OgA?<;Ctp6dDihjLh0f>b8I51Vfd?j>IN|{_6M~`3_#BT~0sb6<(Z^sn9drr|KDE ze|UW+n`0aj6rpf3*ITj2r5k~+>V+louBAp6x62z|)dZ%lw}PHb-m=kSe2vh#PqxEw zix%$&MiCCZj-n|)LMJ{}dH5wT)*1@Z~DM6O@YG3LAl|gms6%Ma9>24==ryEGx zZfdzfHv2v{8rSY>D$H#IDE6Ncn4H@4%)asI^jJ^zrO;4vwd4ruTx5Qjb=EzYYbVq?TjA`tbH}><$lq(y56P7#j;v05b6M&mUJK_&x7+TFq0Vct%|Ex}Gj{5Wt&?9!Tr<*?w*98pcVs>Yzsx zaVmVrF_6?(fix4Z-Z{a4pSj~Kw1*rhLL#(7TK?}(wZ{kJnSvDzLZUDFrL+$g(%RdT)!&u9}*-+ zI0}BU+&)!K_H4_Bh70rS_CG3b?8Ky7yny%(qfeohCsbx=DM6Ozw$+sr#ao!bt;v|n*2INYZ17xd(1$U1HMFLmz;0kkm`T|P zzMFnWGSW4lA7SbcF*H7uninx9@rIJp(T0x)VxUR9xcaTsGK4<^Y5=%YV29Zk_CrXo z{M5Pj?W-~s4|sV5NE;YE6|>u#?f37&UoMxCLYcDonPTAHRDe%>^ta!(*`@YEgxsg4 zJGV~GA8bK}NT%C)ugY|CCI#v4DFm+(xfI8qP&~<5|(WpK?QIS)SY)_iuBW24V863O>2Sg|&)3 znU!%ydrQRnxfuF8Hcea>$seGk#J+;nvGU^R=2aR6n#IW1k=b#{4kWrF6QEo&g@%Wa%*c>Q{M9HK%B3ID>Asp2wHGI$e5Y=-Kp9H)@+~0weZb z;6lEe-u-2oW?NQYA9BB}t}f_)KT=+bbdp9p|LB^K-sROk`N$KCuNQ_$1qR#St#qGY z_4?YgoLGm##kZn_#uWAP;w-_hArXvrdj+TpRgUo>`Q1hpt?SnyobN{d!eTZ1mKszE!HzB#Z|ydT7t-5ZB0dm=*o_%@_L31kaxR@khE@HX|`x zW3E0_dsot^GlQPm7+8-zzh?&MH!`W^&+x)5adj2qXc%9$kDvCf*!$-knCa|cIWW$r z&Oa_3&uQN-efGKX$G;vkuK%^*=Z%e?73uO?^;Vil_>oEV`tyc(cMm?o&EHCf9gFc) z1=x*#Vpn>z{}G$kAZ&gr`}lhJ-oZU=-82 z{r8P&O)m#$^^SV{HLv#E*{v!^o`~Ngh7r^ozfZ-nzpyu?+kShA*z5$>CYE)7;WmYP z=L&zU$bPUlFoyF$Gh_++97h-@l;3rCYqB89UwE~9g%fqp-E0r`NeaRUmTva8AY&xZ z-ag;oU?Re{W=#uZ8P>1aIr20f4DXg(3};ftS!b1yyAJyS)3oY)J1u*lpI|J>qO6IA zKsOD_82-!GR!?#Q>L+)LK@4|>5!yPk+YdCdSIt`g=#6`rbmN0xUA`rzR@4>GR6&66 z4a%CiI{D7Qu$)KYs&ml#+-Bz;R}ROYy$WyBhM{K*#n?P!Ge*GEr%#W-KdEmYq+Wfw z*Pi#DfCOfzfNu{Kx`&5IuG{(M8rqfSBNcR1+H!^(x6b9vU5|3g%Z$d|9$U2sG6kVj zZ;c(HwKUn)UXDoM!K-->IAo@(?!}a3j@F~g)(-a&L%*?`JpYp)ftnb(#gJJ5M6k50 zn3j9l*F#ZhDPvV)L!`&>fv0Mh2tAt`=BB4d*O-?qTqK*^Ajae9t#=5E@HF4iPyi}3 z>>5ndbU@d{!ygh+;mgk_ELNbjm$5LtxQLelYZGc#Li6xA_f*h&guDD6KiT68)m@66 zJUOe&e@-368&>2pxQg-FaQ%7bH}{0=W4Mr`^yr7djBb=+W47Y(Kv^tDVnG)ruXL+- z93QNS<%jx#?j%7Lpz%zp_0}QBe;~T8SZcebk}kV6efF&Ob?bjQNaXZ_GqyQ!Y!G9+ zbAJj$A)K$&ntf}_%vH(X{lVpO2?)$=Nrh3w(Fh|mMqqk+x@-`dU$Q~QFDNJijTSdM zBZhLTX%g<`@Ne+qb`%pZ%rwgjo>i5dbE8%=r*or7p4TvqlHZy>nhfaVPLF{yygnjq zwXWag%S~&Ok8-L6i9b!^D?XO70t4X7(2Us1dmxDU`patVn|zQqH$6Wv%W}t4?x=^N z!czHpwX}QK6lyfDuGB=FJ<|I^57E3B{(8ku6)D)^J7{D3FW(&_4==&%l~y26$=4hp z&`oaK*)qO*S6TO&$g<~mP?xQH2F*!JbX{|D%fr0PZoqdU?-vv((P}*xCPxk3_orYA zFvNzVl#Oxl&4`NiC;z*skFs^}%S4T0)rpjJE#V7ZAAKl|)w1col=&thanW1iLtu;WxkO1opQS z_RsA8R~tW$bzzcKOoV5@hA; zXPLPYwFCfwpVTQ6BHmP`j5*bN{bW}MY-s59A;Zy`%BQeeDkekA_>t5vCCJwr=^OHn z$0am=NI!0;)zNtqI_8&YM%%L`rSCR-MSLaQ$VCtf5e{On+_h>kx3KxjAy4xgh%l)m z*MRWsfHPSxxEZnXPL7U5er^)G7-WT2XkYr<@iExw`{&0s!j7&P38Nf1TJCI-9e(o| zukMo3Vje2TXqegarmK7SemUFIj23)?AHPI$mFq39_8+rrxmo2{?x;F*M97bW35TP zZq4}pe^#o&_jNNy%ZJ}yPd^;4hmvDoVx#AcA565O#WJO?H%*ffO;68PY@46+7&=QP zTj}kU8__VQbMy|Ulp%Pb+Y-9qXkTbjv4S77hwBFfC7awa!hQee?LwQa*}6*&YJQdM z^7UAaoP3-N=cQ&R^8N9cr)bnre{y^zz9P5TjDf%ffy69r_^2=;AD3E$RODr~sW#wt zolPK<>TJ?oN(De^v`f21Fv#fU%VCk8AzY5$b;en*st$5%`5>YST) zsh*5?qwcN7T1DvS-u32rnJbR&*7NKlkMwuk04-!Q&kD}@>=Hf;G3U7;m%0e zov=O-o<77fO>+Uskx_m6x~Zy0upl1Okn0)T2Ks1NU|W-OMJHBcygOMCP~5?6eu7Q* zSv+gjg?Swo9v)WG28K_?HZO4MjMo-=o{o%b)93oJtPholIl-pM@(mJX>n*z^Y|{iE5Zp+|6{u;9*nBB1M$C+!zy8cUmey?ny7=GHvH z0GEZ+UEWN~8O_`P2Bp7(^P7Kuyr6EzkChZ9D!`;>oPa!WrfL%~$I|B{*%~#a1W9k( z4{-BNZlvH_=qD0(=$D%V9rtZrr2RyF&aBq$*_+8?r!TQ5m24yu(vN@4-V9pMmCYv# zv;nfWBo+H;>)D?y5qos}6Hsdx0Cp(=Z#!9ln^7ar{GWy9_?JX-(SVvStSdFP(!C2G z&XCtxKbdT+V01ltNz?az{7q=~54whn>aAJ z2YB(L_N`P?9W0r6UYV!%=7tv%pD%zaOe19hxITHZg##|_>jQ(j)Z&_E_^cSxg(c%d z^mPc?Af50^cZbSb8X$bdB6@xN4QSr;@Sr7f4}}yk!bLn*4zSb>%a`uE=#l@q!nn%P$kmn83#!l(B!rQ*s)AojqKSlR=dqFE zb;);1OJ-SQbf*JvUzsc&53Q1si?JOr1+R&rdUah36}-vCr8hSi@4HrXt)REt@^6#u z(5e^GuV`(Dw1&K{_#rV@vPn!k2ZivERV!geW{hk?_NLZ z)#MFm9w6!0hj*Mt%T0{xOn2J&6|bf7-u>N%@4|IZR59!8rP9*UCi9S%u76>ZHCZ=) zG|?i23kPGxRBBUav$Ca7Qr)xB6*dgf!^gY7SGaxfqV{q7lP{|+GR{l7tsz}^N+Iti zfoe0E5SXKmhUJ`u)`YR-EWt;F4^iedfX9WURgixNj31}I#f1JEOpCkx`K;*ePq_si zit0;C^^W`_w4@3;8npdQ+82EMu$dBez{>Itt=0l!-bf$Mt8D)(6sZWRyc~Wi!p}1e z{rt7N$q0#UdU|^AYpHV%A|<3qJ9y*K{x`qzgLT!JIW^~+L6&uT8KM_1Zc6x;08vLB zBtHHcG#;M3d`i`p`CBG5v&fmeq-|YQ?s= zGEej84e4WD=pH$)R@c9DQ*6zMYy2cyWSA(j+mEgap(b+Grbkoc*D7}<+W)M7 zyVBi-g@F>yze&k5r@KF_GF?IZN~lFxZY2EJNQ2ij?dOPj7cjbsv80_+DbZ;Bssd@T z|AZNJv)>1=C)!(IuB(*t#M^9engs2!j#y3S1`NVlY*!mSFz4f!*7r_ndiGV5w%+K$ z60r>!T&LLORF7Bsf>G=O0518}9qsKGQ9gwi_QI22efLA;p}VJ?ZPC^KN#@dQx3J%$ z0y+-b@k$*}?Ax_77Q)uD@(cYJ=nl#*=1B_fSEdy7vKfmtVV;cru3^p7?Tp!t6_Wyl z+ULilvq_9>aA4V3jUddqH4w&(49&1M+{6CR*27@SWZ)06TY)YBJ!caEB~3zZ|+8l^#qNXKU75<{f!*?&Rwgd%tIV21txllT<#y z`1&n@3VAksfIOn2=@ndYnbP736#9c)x|n(OoUraB83$u#ZBjnhmUK?z{}hyFS2idr z-bl5~{A_x51I%<%r!{NWC9ZEdc7EsU5CYWMKvF(EwQZZFTY~)Hd=Pzod!OeuGX(#o2|Je{B#J1Q&48PZfdNia7<#NuG{# zf~fJ~=eZ5SZNOjQ#31HI7-Ld+^Wtx#aMN6SJhii>HB}CnX8CTHT1+1d2-&(l?S$zx zxfn-`0Hs9Z+?!F|$tD{WEY>DB+za7ud|;O0QX-bbCli>1(v)^_woL9MnIIT&D8DwH zOmn^@DXnnKMoKHnn%USk7RB?-;bXN?$!~tG>~fptX-;#GA~4(GFxQ2G%q>$&KF-1F zm>pARvQXnEczu?Vl-^ZS4Zje_z-Y5UmU30So#08Dfw7c3SSY#kB{#cjr}HLypKH>O zhG%fPQu4;V*7>3}Tq*cjG=0;Qw!cCRvJCy^)}4FZsy1IaZ8&F}LUYW?Ece90YLi%b zKgC9m8$X=h#cZ*UMw_rQ?(CuW1d<&hX>GO;oGRC)+WTrtbkFaz4M3y-l1|j4{lCxl zbt-U3=KV>PseqmfAu+T2z3`v2sI?&R+t5}QwX zvuZSwA^1}qc(o;#lN*S9{ypNIpuY6jSNmn%*F0*|uHnV7R!q zEcNW)zCtJzqp`&(Qg6sTRdtrMn!4Ay3{};gymXOAu#b$SX3_c_fZ!e(1s{YkdP$`B zG7A~2HK2?Raf|MZ#O?<|mT^~-!7}ty0$7~4YsxGb^TInH3aEs=CJ&G+$CUu?C)Ta%7^`mO90H<>_Y}Hf7TrR4qk{Q zJ7u<+ocXKLeLfE+L6NYrk5>EeZv)_DL?pKc6IclNlFPf-ifjj|@@kH)uV4XrzRI9%NYf%fUM zKMsVbB;grsemiWhDV!MVn=Sy)WAfbxLeuyP;vikWap85xkL1fVU7Rkpht=NEzH@Y_ zPSCv0Dkss6MBXnhv4H0_#h_N)Yr>pUBM11ZX#@iG{ZvjmB^WoiH$lkF#A4sS&V$Xc8GqGMan=Nnmab zT|Mu(HQ9iV-@O1&QNAQ>(;cSm2t(ORXc^I?!A_oLl?~h0c{;$8n&m7=^Jt7p1$e%= z%k#rHuOO4ICP~Jx8Epa+!=Buz;JJOq+ryzcbe%Ze%?M%j_k3SfY z!F7>-K@vyL?)Z4?qlfeAdJ6iB{i=%Tt|eld7fxY44j=aslWZKtEVoIhyg5s%h#5LV zD3ZCp?+M=cdc#nq^jT>NA?6he75e*|U7Fknqp*e?FE36)wOPUqdj9efz_Zbmw;;1} zFl~N@B6FP{QrlS}TeAutVD_O8o;l7{~*t8dU{Wa1fxmL6TJ(4(j z6lH6l-xr1@Ht#c$a#%JG)`Li5=3^c#oE1*U1Rn_(zjw8%X+`-U533FjlK(S=qalI2I&oXrw6P6*8QA#S(Vj`Akxy;|f4eECTZjqcW@*?TpqWcpkM46W=DQJ6K;yg$(tg^n~&GF z#&CWPSdnuFsUc&@+vY@{NG%uYtS^mFelHYRM!A4D|yWo73zA`+TWaTi<#i{2d78PsSlD@MF@; zM`GUdSiMurhwA2g?NeC~m$@AI)HW&ZceeEMFi(!)Xt_n3aVCaHOa8F+79^7PHEAF| z?uRJQV|7&O4<`Na;l2n=!pO{bJ6Ny1puZqP#K;^A?`0wol<10k^x(Kqn{e)AG&e3X zQn8`$1!3$)i^2SH5?-Y~UQ01@cDBMqmij4n`HH=sThs>bn5q_>7kaD9+<*Dy2(K-a zkILjv;c@TZ$4|~G*!hx~&#MQ(VENdfX#+mZFG_`1j?`(GnLeg*%07$J4kd#9*rfAe zwAg*&-1IVLgksxO(G$hv7GTFGduSZ=9l9G<{xdbtqHW!ps88*lRI`HM!%z|8Vv9Vz zuldNik+lGt}E|-<5Q8cCMu$tc!vj$9oe;y!2i&!}bO*y5QWlrONTj^(J}I9-tY&T7L)e zoOh7jJ{AS0dY^_*?(|4i`@_5~AXes#Zm?x3`DP51{14Hs|bW6Wrgi3xroiEi`JXY&pElWnli z8HC#~))~uLxh~I4w6Pu>^dYp#4rNm+vIa0)_B?S@ zeu2D^5Fmp1a)e`h$fytnv>>`j)I^T7ZEw09!i{TlKMs=s`-VJqM7mXB+EKoN=EDB_t$vyvNzk3iU>B2OZumF8GqTv1cQUHV`!1Fel3OC)2Es zR(16gmMn(@k48tn@P^mSYGKVrZi1xxP?7}Hu)5t2Ac9ZQ95gO0N+|g$BSr8xyB)w( zsSkZZ%|4CaDK~vskiobc7fut;zB^mf8_lU`vF$E{vC&{T&t(hF8qKigPmm&C%$Hr6 zd`I5l{4MyFW9hFeD(0sxVDTo7BV!9!u-4cBb@>RL0K(%7NS#GdQ0PIVRYlJZ2H6^wy+J(44XMSE9 zWgDuKyN1txKQH-jB>IyyMq(XB@;D^vkwB?-69(IE<7IyX$u6toq;->CzD~Ga;Ko(+ zWRFRd*;U@D{Ni@_A}IwBm^TXA%KE9v4I4r(K7~ULXQp%gm;vNiSoa0$J1#*-nB&tr zVys0uT(@rtYBJNC=t+!2_T`t?oful_9yvcuw|PcoR-I2wV`tjVgxJT$|CE2|LX)*#3Mv!k%UNstq+$;=E#U~*1R zJoPJEi@y&7kjOu(BfJ8Ul>qR$M<$BP1syE>MRCFu zfX%u`a7VuKQ&;;!!CaQn@Wjt`V=(6Z&1X*wg*!JGOv1R7w;FlnXPB{WMcBQ8p*)|# zEOA^oN$1nHL3;@3yjD3SL+6vxi&-%+sMdoo?>u$|=9Vx z?J+~!5dXgYcrOuJ8AuMQH2khO{&IsC=a&-Lx{JbTaxCIr)`qe5v78lVv3nN{bBB!C z>qkkFF8p~iHz)wbAY5h5zFZ3mD(@b~59K6He^WI=ICqV9>9K9@+9%Fo;2n>uwrrAl1_Cep9CDcX5Le zXfC6PK7z07)_Y%R;~bK&@!M(t_quO7HQu?<*jJId;?D)#F`l*W_`pNM;?KVl&eS_E zLf5(5PdT#9-FIg|gwL_tVX*$`L};q?Oi+~9>0D;Xz46w+4B z9O=0r8p+SBECNf~V}?bnTEwp9`;wY`+iu9+sdYb~kLggBo$(b$W=kBJFRwpca3y>% zSNEg#kaQGfJ{PG{goBzPlZ-7<=cQzTQ3`F_eGS4X0Sr1xG@2j)%v!q8~B3Um1D#3?# zPepo5otEI6>wCGNscg{m=%J^%qHDvmN|?t&ks?NDj+LnnRt%Q+f(?x*(ejy1^pD6n zVRh|Tj*#x^M@joOuPra7qy;n@BRf=~ei6^LFP{W-q>Yh;YVw(c3nFHpJ9X>5rB*vR zHAQy2D(G4q)zBVm~8S07A{e<)`b&hh?WJA^R@-X*}?s-^Xudm5^GmiIC7jaBaBSf zhXv*V zT7%{*xN!S(M#1eicnSe3vVYKG1%GlGA@p~LdNci84_jyLTj#KIk;7osHU;l_yWyb+ zj!uMv(;&Pe3AQ?1FHqzh{NEje2OEPOzn`~+?yPdNecD&_=S%XWy0cbfmKF3yhX_Zu z%?_*0la_Z)JQWA^xjWHtQdz8_Ae5M=haV&G176i;pL(GS&XStwv~OXTCmL~*qa`awR$|3On$ycEx#$@)r-+l%7yMtpwTRTQMccodCZ-NIoTBDTqNzx-2K!J)~H zqUM(H#ZQEQFkhJus19cZ+YdJ+j(=#-%;dd(Xb@GA`rwElQ36WD(G^5nGHF z7zQarX&1Up2vLQW+}NxyS&VCL)O#_W6m@2{&q$vllpSI3q&+& zZa=`&jX4y|_76eg6P!lX_A_cjdw8}1FPU$ojHC8E;36D{5ogFY6C+k}#I1x(x` zdfeoOM6``=rgQ^bK10j{U?`>&Xw_QegJ#JoY)<6cae z8}2tzA2y8(SBdOm^2QGd-FQJNGj2hsRkb}gi{}U$H_6Fh3k&|_GT7?%J3MYglCR_a@-_i2EXi-((WN3*+-9w8C=2bz=SP4vNUCL^+OSqysiD z_}qWP#|kabzUeHF@|DI9GjP%G;HNRd{e-x%vH5E}w?pcv8wLD0xW5|8ruFg^d<6>q z(@0^!^h>e|P;k}rt!Wc_Bd%b$qdyTp_I=T#J_^Tq*YoclA&qDR&w#HMqr_&3lqEXJ z8za$azbp9wbMc~5-aWo6sS(IGs*ljU9-)8+!X%~D-f?1LB3}|dh8GDiH zMqY0?UsL1$yRy&6EeKVcnUR z*pqMF<;}ilH2gv54fWm%*oS07@N4HXWTY@BQ*E#kw0s1l1n}{(^@XDxc2l)URAIGB zqldHsh8ODuj_cufCfQ9hQD8Ltye8@@VVoF@+q=T;q)e&vP2d-hLm16%E-K=SD$(CT z7H=3pP34~4@8YtC2wzv%BBfPz9jWh3RdansSle<4cGvbnEUvRD;{_#uom@9Rli6x| zB13H+Eczt@AfW@FIvC&ktETf^;RlYG;YvDn%!-&{5y{WqPcURx>LPzsVf-ZtGXTlPr8s}s$1{2# zJ87Br#B{5^zVF%*=!ucOtMz2sk0iAwD+@*XB&Y7hJ{-1}xjhH7wrV=!waKnSKi)V$ zV|W|01#xaR=l~`G>fs;DRT=WU{hhwZ`}@`AX+OFSm9XG-v=TA7iF$1mI?nKvwowyp2SLqMkjque#iZhwQs;X`?_ox#v2eQ@^ zo)nBd0~GXA$D&{zpEy*y+l%uM_xY=}87l7%J-~Gn{P8xs9%64mwBG<@pj^I~rn%`6 zqWl@q19VtVCnBm)Y1SrL#d3z5Ce?)dZb{Ub#sa1IStPX)4e=2iKh=3eN=d$}nPN{| znL$?#OAEvgm#KXvH`^aTW+i3Y%W4(C7zhQY%zm&Hj&z1^1!?><8@+?Hfa^B zSTy=B<;OT^6o}!4-N6^2yC{!aQKWU{w}B!t+@X-r7VJ&ut<~8*;{TZ~Z2|Hu=M6!lom_C%t=|IJ!j&P9X)bR~u;0=3%rHij40LH@{xTK5LCU@Ju2uk)4h1<=HUa`x|_ncIm-t-b0;irRQ1#a?EW zwjnN!<#04ECww}$!At0u3zvLuD)Ldf^A((1Ho&dLb+pX<;7BM{ev~=fgZv}=%^}U@ z?=yLE3IlA$_K$r;xg`;pGf(Umgoad%~G4!}V=J;xIU|$#*uL{O_&N zH@~5V(HdeoS*VQnWKB|?mvpV{L+|vo!?YxjK^>&0i@*{h4W-(FKh|Y;Lc0~NPCRIS zUmfWIdX~$1+`zjlrOQS#yBxrJ&+D*zn-H$-cw)-J! zHrb&1RzZ>@;rp>>(@#BXTKP38jS1($G%HvpSmFY2q8_NH;C36<5C<%mgu1)srq?7f z#5o_f3bR*eu1z?3<)ceLk`)dL`j_$}hJtxIi_C|b@2*{=vZZLYegO&eO1aa1*rZzNB;#)ONET_)Idz#%K zlO)Q#^p1s#$AxIQ^Y(@bXYh+_2gG?_Lr}g{*P<9LAlNj>>H0-e{ZDA#;;XN(+0AOZ zMatE7QB-#Gf{!LBnf87Ccod!1rY9bu;|lX5+$0PrSh``&-GF_b<#zSQt+PHu_o}1c zT>*AjN6lENN|DL$9)}lhnW==Wa%2OCi~AtaVK_`v`v2%JY%@KQ^aEk^E!0%gG0p0) zA4;$6UdKf~aYmD$#T{4sr{E0A1xL^aN`~KNjPFj@zyHmJ5C^05C5Dq;U@{v7zw+(4 zUrxX9S1>9J3QX;_o)X{q8LanwJJ|PG*2$>rgx9aPG`K~Y^oDthMRr{eMx8mT*N*{n zzcoMg+ZLCR7r5s}6?$|RGGq!CO%QH3U*2xS?tN(r$)Q%{v77G;H!qPyMwl1V0ZNtc z3os;)jJ?_dhnPO_Y`svr8Mxebc{`lAQ|P`7^Quz%)l=A`UA!o(mRqv_-0|&L_UF%* z)+}3Tvi{`uh~?jvH2O-u)(T7ee~i6%TvX?_KF;}i6K}2>lGrdP2**9Lse1(t$XW#Ww$`Gekg%|Jv9M)2-1F4dW3Gl!7P_EG>JjG8RqI$7l!1%T|Sf!Q2a2T0XJ} zUzzmwda0JL8WIf5+-a|BrnOQ0@+_yy1CVvL4-Vg>@cmoq4e&?VPpCi*PtMrA{BGvK z_TC!w6bD<0@r!WJ@@(L+{!a^@oFoB&oym{b<4g` zV_av0q`y^WIBY|%Y;bc-lImor@Ex)?uF`|{0zt?5YFyYqC}0Ed{*du1+BnrHUJu%+ zV10HOG)dlB!~)1TVMP;!b#yM}%Q|X4NbmX*ejqng>RdVg@`qp&U?Wi=zT0P9a}9qp-u&V zgTX*OMA9udQgrhI4~;o(_jo(2_r}sdXlTGXke(UEJX4H~2TKAEWyaKV;ZtM{&G2B$ z?CJWUo{mvBSPkBRP{4Lfp62-FJpqJ~U0%!26&?+9?V7n+deN-Hgd=gzt*<2FJCiVx z3jgWs+-~ctLzH$~6<@e>)6KzBPi*l%-vu#70P$V7?7w0jaE2Py?E42wwe#U#dOm}g zu4J4KVieVuZ zHg+t)3*X+}{`}Qd$m1?+$r1(*bEi683ejStQ^S zoyJCCmtKmD2v1C|FObPsAiCODi$2gas%9SAtvXIAj3N#};qLV~7IGpCPV#j-tt1 zboP!7P~|ODX}zY*LPlwSDN^1Fu=LUgpplnDA?5jWjYFcStmEnmpR?&7HjxE*X+1nJ*rxyR*n!#V z;|_6iW_q-2iQ|4EgZE2##huorNPXir6~bl(GDOqyE~l-E2{1cc1Oi4eI}}pS7r02v zpyo&d_YpFNykkdYV^}CKpQ&R6e_pD{5UgC`59}~{3`T8(`C8ZJ`3A4?wNvT%kpTR- zBg9WgJ4g6C*gLRMSf!=5(Xt;!#y<`ll*Ap)MAgj1mAo{p$V{qgTtQcAT7R6P9@ zg{6IB9Hz_ym2=;$FMdKCg-3m)(JBWcZ?lfHPc(8=LNZKLhS>-``0~qd(>R3{rqbp~ zwMpc8_ZKSWBc1T!Ah>0~y#k)X*NklTTy9dck>cMz@*On-(7oQtL>NVzwzi%9Lu2^6 zjuc)O*;gZCdD@9)rN$j^*SVq!sm|7#uXT<0l6I+r*-d5ApDeT$@qJr%aYPA%6rP(4jw8k@i5!u8fpN}VGQ2ya+tLqLC#FMdQ}d9_Dm*p22Vb8(dI z`oK{pIJz+;ZU_8;OA15}FCl3Tow$hjG0^jVtX(>JyPIzKTVjez`))Qorq7rt)HKFS6WBB(_LDybhl z%)e)HHLlyw-uPu)2O@E0x-yd?%t6@C;t^W`z^@4yh{m(Mt~T4IZn0jN-L~?k=NkOp zmM_;Yw+t%c2&ouiV#*ak+UM;wD&z9 z?)qWVd!FO%`uuP%#zC9>C7c+TjXG>mF@S9pUin`r+q6$rd$M8QW<;^CK(M_5zEgK2 zlbtf{xvxh#OX=w5%SvT*JT#MB-kD~(8XTjW_e|uH{&k(Q(Mq9N9J%R_NDDsKR)Yk( zE&z!{ZdoIRRjv;Afe4gEY161@n?JGCz5n7BY1(i1=Q9w2DLdrr#?~hXN>UC#`t7h) z4ZJe=$bOD1uC6y>mbu+nb~a~@y+hZ2G;vdJ7VoUhVHr!9{1eX(%YM|i9?2kum)KyE zB2ToyE;oPwFCbp4`HJqnzGMj56RqLc@I)V034Y)_;cNT)Yr@gk#uyC6_B(%E9vf(> z-g_px(8VHrQ5gityOo0&2b+;4@rYjjB6JZ)Mq8_HMCZd3al%cEDQA2a$^{IHtHIW( z1JQ(3WTp~<0gHF{18}lwV5;vGiGcV)A~2RpRpQxb67dSi#fuF@6rL zk(y>RiZ@?sI`puzb7O2l`OWwb2>u24Sft7wB`h?1I>S`B#q@GSSa8Ea*nJNe+wYE> z6U41k;bXD&ff`?H%c>lEGIy)u;_*wdPyEJ*@U@2#&~JjhAO$ycfV>YCs@C1pjd_M7 zi>{(q1%zzLzbvd(VxrohmvvIC_WSx+laiw|>Fw!acUec*+*M$M@$V7%54!&&LfwD% z4?_djHui=Tdnj1i;}l*5Fce=pjgx^dU5^@9sP47-Yb-SkJ>LI-7UE_;DFb5{4n~aZ zs_`gN${ocE5|E2Tnaa*&d}{Ci+xXUPHQbS)QMd+9=nuFmb)WO#ptG>{r54Jt(eVN- zu+w2djyFKoAjB#a6`t^&7Qil7vu@?T{|g+*u;VHyJvaWCMbttIs@FCfcb^T;>UIa_ zDlkS`63xNQ>+yXRUWbG}QmJjr5n@}q%JeGw_7C{w3JlFl@91-@>z8-$YHi(7=PWzz z#iyX|gdn%evi@k567^a8V)Gx*lJnakLcUB$zGTYVS2Gl23vmBLSIuz0tq16ej`pOB zJteMwWwMKvSEaZOo&u$+m1nuN-cJsI!3B~j)S0nEu} z4!nvk#8>_VTr|Au$MK)(#+rmpOldz6qRv0g=t=zK|{$j$MuxF^arObIwQoO$XcMm6_d40_kRMqIBRGxSG))iF1Uq=oX%E z*X=1X9<|Ku+|Mx&%-uR`!S`Ulg7wPdu>!;AYGXFd}82z&u-@xKsyZlk&3a;zEj@E)6Z|{aT zX5|5(^y(fb`-&p)7Cm&J!Bb%*b9O?{o&AGiii`bf_d?5tyECOg#f(HS_ouvpNCp*i zi0fY!4;^aq|9ZX%0byaH+NUp)kU0HZj-`s>VurXM(3ykQ>&Uxrpd5dw@Y$=Ce}2HK zW13&0s+{<0Q>o_wea*%psm=H3c9TCCn+bew*nApF`Gru>Y^%5C7NpUHtAKM)b%#nbC_Qm==ncZ6xb>W{QeCmg7 z1*!EOy)QU{3!&i;&YHJgT=fAycL(rJ^U$u}BiAVSLc3?{~^vC6(TrcSCZ>6`PUU=(|hibROp9`IAgI2nr%JH1YzaBN7XG2p~%) zxb&O#sU`+n$v!>ikHR!1AUYM!F! z+#txjEQ3mD5?3)Xhww-q&zyrOVHKU`7o@~E!U5wkKMG8AgT^v6P@ z&w?DQV6!58?$RCKS(x?0OoM%M`3Uxzm}99V#L;yJtKawQW@Mg4m5%= z=OWzO6)W-B|4B~axd+T~kAcyOd^qAmIW!BI#Wz_4x1lN^pF9Eq;swqPWOrV0RPfwzPyt;+< z^@I1BF#H^BK%2W*An07Wf1kkclV!f_V6gG!cXO)iiCl1CU;u6H-~>`dfcVIZi#=_b zd*}}+1(#N@0Wq&;c%&Ff$jP+|3*-zJRPPD{8lBx=LXLS6rQpi0Q9hR}B#RK5>~ef@ zkGS9*YF(qSrlGkjzD7*h3cxkF^!f2s9m4+~quwq0QsVUOSa)kx7r{8TOapLl`CKp> zOW$R0=JD8CDqGj*bbrdfqG;rDkjxu5B-v#!e_l+YZ6_EjRD&6FlftYX|Ncqb_)xV> z_r*pbClJqG`3yr|@y0;SdE{N(7)ExY(hC3-X5OaGyI3vOqdGxJ{Y=P!kXQt&PD&q4 za23Qydwk40hN{8J9AzHxrFhdb*l@i8Lga~G_zp-kpG05)iZy!--X!V0)GBS7)|3xvwDAD5$ z%X&!#Z2o!rX(~xS#9(EztF57ywR;}S0%?{&2KnJmCaCk#@R7Y8Y;rA8$*HU63O@Mq z<%Y$Gm>?o3kVX*saC3vLkRT-{_Hu^^hhwQGc!q?bm4`kYCP~r3EY$$?cxrXx3Ad+d z1+OR6kF~S%N(0re#UyjW-SX;WLdlojB>bB3QE8&9xm<)XZgm*OIO7EF!24F^dc4r? zV1+e72LtPRybm8t>2y=r?mo5j5gu@CLqSY(o_76E(hbQxE0%l4no^bcu(mrPHfOQ9 zw<6i6Feh62aIkWeKvfe?_d0t56mR~M=AAFMN`3fIiXc*BO?f>ZfjNIb0*@^jx_QEq z$Nb8Kot+&erQKHF7rd}|sL%{O0QRyE6&68YArVB}cya}!;MEw}Hj(#mKKhpqd!X*a{>0Hb{u zY#N^Ef@N#&lQVPU6{^xkN0D^293-{5%Xj@yx3Z9V&V9k;qzYoWlJ%!05rSwoK8pH? z)fY(;!YP+i_5pLI;x0C*7icD{T(J>fK^CQk;vya01UB7CMpt#(Zh&S;_2aXb*_&67 zKWF*Tz`t|k#2cEb-997CAu1$nv^RruYp}6 zjy6Jj#+z>b&C^~X9FAA0QAGJKu}CVd;cQA{ZMQ5jW@K&Z&bR_iaOxZ}!|y(@UB7sO zTQfKLkprj{yvyRq6g)hkHa<`q z2S>8^zBSo@_n0@U+QCV80I4#EZsPMEi!Tl^UMP!^v2x!B(789_>3uoU0nlkD)oDTT zkOXW8oW2`XvIpop7oT%UFM#9Bd=l(TE;wgoAng_YnSgz4<#m);>UkuWnJ$ zVD$kXX7ipj<~bIq?_RhO#Oy|Z#^c)RsV&}G58%-Uh5OU;kWF@Gi;rdhtIG8jr5B{K zEOhqa3;)M~{TX}gwT@lMZy&|oWwD_2*V^>&PgUPt#Gw{*cN!?W=pG2-q>qVLXpT6c zTQ2o|W9qbNRo^Xt3E-}slROH>GZzl3n{7z**bq#-);;jqE;uRR#c?!EAUhXUwwHA_ z8r>Nwt^E;lX$O3QGrn06O3Z}9_II)oi{r02As2*ODbNah*Myr!+_87|Gv4tqrsJBQ z8IkMthSbfoq@*2J;W-^U+@=RcDQI~QHcr#kJcavQ?)=Em3yC8au&24h>Nsc+^4a-j zS+?yC;cxvakWYFyjJ!b*g}5}2&~VX7;~pXlhFws1#`?>d$jy3oET3x?Fbupi(mY@# z6QE#2q!W?xZp~K3*XS+lJgTj?Dmt76jlja?-Vm42tku-i@VHAM56vKYNeLB4tG=Qi z&T+aQ{L?{xJ(O$^yLi2UuuF1#JIlP}!m^(v74KJm>=jc;m((bij%$n`QFR!3hY@w( z8p8QVk{Z)veBSmGpLat0{h#U&g`ko%JepF$3b{efS#bLs=R7?~wy3~no;1lstWG-6 zg%rFD2~iK0&7ky*3>{yp{fT6cM=JUU^5 z1ou8s^io#ON1l4ZADg{^H(I2!-G2Y|7H>|lT%fhZ3s^nOC5w}RSDNuu z%E1*iH^=hjM^p!2>%>71{=UA^q^bDx9GJ)0+W#YLtl`}BI)q`FhPv78s&=ZsAt|=! zFo~*SPC8x!&Ja?ZsfF7;e`#q?$Gk;Vu08cko>+;nWgj^%ENrv$si;wE!)W9&!sq6n z3O3e_);JPuZ1xtZmB#%0!mIUKGf+Fy&uQH*4VuAAaB-bWNKVzumBfC`xd#Da3W3Nj ziB@~nO1KjgJ+F1*2j7U(4k`dXe_|fjB(U@4qJ9$Z!6+^Vh2ojJYR6h{+mYLd za=y?1Bls5wQF*t*2aZ69kyFx7=0xy}O6CMP)@}?)fAbquGzI>ObrJw13d`Hu+RhTY zBE!G|s!c;?&jKRCw77+~g=u+5qH6w^(X6fANlEq$>%IWz13C^sI?lOB38h?jyVd>wj(I~zRe?%zuy6-Q zQ_Fc>lw$F^^x=jyvF7Oki@S1UZ>-3Yje{Xf!*v|9*y|#mYHrKeiY-5Gd zT=Z~H0gHH?d;t^^PoL}<;pen-0)~^=@qtLzY09Z{b=J+Hb6NamPucQVR4!jc@+E~S z!4c2PXxirC7Wefj@x0w|-9=gsltf3(^AA}+mTq9oXj`Zo@R|_C*3d9%I@NsThM6>9=X`Xk83(TdZ2<{qe_;{_`(rnpH1af7Yhu8>(+5YC(H&DCE(t z<0xyJVu=;~K-+2JPKxf+zlzDSynn_Nxs0&=Qg05u!Y58x$i|>dTty z$GrAHFlGpI#%le+og(y+v5Ptlf5IElrt)11r{N8(V+=fm4;d*<I^!aEw;A&X0Xseo7;kKBgqFzyLnfibZO{E0^hw3w5Sh?G^{r5jTc5jZB2*EN0~~2GY1kd3`-4|CxN$?EV_B*7;h2ka9c+7F z+w%Ulwq=(SZJvxw=x3HTskbY=TY25mvDj~_WsCbME2*ph%vhgq{#M+?(O|60LgP6* zRB%`OVCSO<#s|%=H(yj2C9QGdD57I=s%YXp7+Nsp9^gRRDOnmg6{}GrA?3&Awb!NQ*D}R#?E7{2{$T?DTNG^w&FM_6^dECcWl$jXn}@D3P>zitvwi`EDo! z%TG09cRuaXtqPxADv;D6%DBosbc9hza+bJ{=S;Dy^SepTia~!G(Ljxl&e5HZl z))7Em(}GQ8&xSN)hDHF*J{CPTTEX!ajA+1NvD!w`7oIHEceK_p4>A_k7Mw@#o+IRD ze4*#(KF}xwfM}bF5j}C@MDFL?7oU&WZbDpDu}wp!Aqf}s`<})>1P&G?vQF@}-OtI> zKVD-OC!dr5f8m8)Xd3OFQpV?4mx%_*SEi9J>%Ln53lL5<2L7x(L@bh=@6(1&vCVFV z@y8E>Om=S-*?~8Pt+_<<_~>p~2WcPs9`P9c&x5mgweLRu`CGdVm*!k_I>#l2ciJmy zsQR`hhkTD|2hw4lY|@W7huW8S5y95b_Z+-u5=4iT!!rgUjl^kk8yPaF#i_&@kvWYS z|6+Z=%!&f6ojdM?Kp%(RB{#yeS>>nRO7+c`#q%-O(>egJl14TLIdTIGZL7PwD!a#_ ze?HaB(8nbt=fmp8#XKKj%p{|w2>%#F}MO;QD6>VNjbgD44fjK#aIuR52+v{I7(%6T%jiTAH zvy02H2pJpt4S$&oh&n`HC)IoK z8pF=7_~%{uX36yav3|}q&(TPzuT$(OSw+`Jax0Slwf`0()J4IF{RzWmFyO-vBP7s{ z&v_Z+omH+6mQ7zMtitbTv^rB4GtV(gZ_XZpg- z6`1sH{N=yIkO{PU3`gnZj`~P_oA$O?z3=>$@iskRF$HK{INms7*{Xd{mm=@|p!J0c zGO-%OFP|X5n%{c%_ZtOgetGP^`I_*C`#8eoPNg@g*Fz^clGx#FLp;qi4&*V!Hp<8PCffnG1JQ-1Xm`3J>&Nm>SLB-nFh zc`Sx*QsX}n|+^5+6+oBc=-2Y*N=A}^TJlaZCdAq?jtTqfYDt&f~g>U_Uz9FnD93Db@^-Qi} zQi;^)TyQao;c3CH0C)QWLhbt+udKB{gmf9&kx$cWIm=4%%VHZYu=w!LG;!}rmACFf z7PRoq@ftc&ukGE#TGU53? zblCRw-!Dmvje%N3APcs)WsV{e7p&{uImN|FGT-ji&;<8y8ue>-@sCJ-3_Xs-6dTA- zqG5f<{+&9p+LBiaf9$y@*DkGsLJg=`L;X%i8rC>5l9B|Mo=CF5vFo)onOitp&=gTP z7GHF>cEK7E{3S?SU1FiEKIrTfDRy?R0K?)T<9AJRT@eM=fonGixkz>>8S-ESU0HkJ zKN!ns9!dCNB7BI$ODjTM4%G~fcQ0(WUGMI?dSBXxJims(1!7|>L7-&1275PO77Pax z`s$(fz0h6qt1K+qA03-cZ-&`KEJVlhCyGK5j7T#LqE|EN_2}|e8vyScPgHDJYGhtdJ}?gmlnUohAE{^5q!K^0f~HAaYVhA6 zj^QEc9zCa0MsG^5DewMLMPb1jgDq;(uYHGc@t8SBtdI0X5aGL9ios6`@u=s&bEhWT6zBe_x^9j zzeQzR{Dke01bB>62I)ZER8oIT|e7>mbS6HzGWyl)|*DPLdC14UhVGQ15<#B?2#IF0{!Q9d6 z%r_s!wcPN%pN$(_YhMs9D`gw3c%bP;rC^3M0GAjh z;1Wqmt~-V=1b=T`lY;A=Zofx~P$|i=z@`kwO3R>Mc#rYD3SPj}Ve&lr{VAhMrD4)i zZ4lpZBG_af9O&$bArsiF$YTnWmPSzY^YWV+6xLJ_B$J<_;9Q}!?C-0&r#`9XaQ=C( zc3v+-juN<`fAasb?J4d9{Vjhp;x>>cNj66X>`-JU%O86Dd-U$gFPY;>n`!F0NnM)Z zsShLRf;Q6C5WUKuzy4|_Y2wjzHI8R#qQeyG4Bpet{Ug@nM9Cau^ZY#1S|?LHY9hRA zXuB6%#8yQVm3d8y9ZGo_Rm#EOZuCE2^!JZi+OgKaA~&pl+$N5%0B`dfh`M}{RK1{D z>}3G`TK6}?h|O+9V{A)#(?$28Gnw`LAzbUe=}oC2Nz@?^yql=SUCb?M)sAK<(Bnp^ zQWyT1#c9o#T9tGlP%%rM&_YF6E|klnI~OjCP6GyTSsX$_^nH1A;<6~q8tYrl>K)D` zih3&Oe!ELM9Q+5{D`Y08Q3sJ}lwq^z^?6Gd1QM&XH8S;CmM^Ys>bl`;)&Rp2s~MZq z0qQW_b9=^WxIjO)u*Q(sd;l_&pVQMGDjcw!PUqae!KLldOd|gvw_v>tZUF;U73x*K z{u(g=d$iF5UO+*6<~SL>9k7t=SCW1Zd~5v8>qD0(e>S`cw9Pd2a4cg3rK2yX(bn|X_120Vyd$b6FOv{K22zm zobY9uAkcVo_{_)ssKwJ;C%iij@Co~a?h%jmQQ@X{%p zpMRGA*1t(@hc`Ge4}A);Lc@dNlX-HP# zi&7rjXmD#%x;o;02Q4}I`8Uw|T3ZyYW`Zvj;qHz#=?<{rl2h$?uxYF`0wy9GT(BGF zAZ;5)U67nzBdrYRZh~SNN`~Ab)Zq{?v!mQxN);RuKA7AS$?j3RR0`g1#rTiT^ z$$EbMdg>u~H6MaGbxPY(=>m2Q&)U}ah+Gb`88yqiSVb<%*cfe^!*A0FjBiqh2a!*!4Fk?3 z6YH_zD=6qyuIF}sU`1GP&Cq5?-4$ggo4&mA2Ks)GGLfYg2sbNq#MS0$!u=ire(-r~ zG?(@bUhH|2*`_L}_WfXl85H;uXyl>J-I=iDQE;krv6!&l+75+mxzy4e*hYLV+zcL` zsZ{6pk6sF=hX=9wsqyGag%~b!*BN4+a)nA)zOF-%HW86M|KcD>(yWmVi+BaDAbX}+ zI!uBr=kB3ZZ+zspd%-a4N@ne@y9$CFE`OQ{-^6yDsFEJFY&UOJ%`;g=ri9t^~#Hl2}qAApK?Se zsg9h4U?^OOnYr=gXI11@Kj zeky`Bif}@1PH3Jz`a`IVMZm~2y#@%)5Ut{H4Q5=CdW=}np8ROQG%4dqI8hKE8O}F< z;aGjp`aj=_;^&C?TY#n1GD?|-9l2Ir)xHleO#RgLV42)oJxgwNc?1Uc)%zZPGXeVv z@A%6)S&vRj&Kb8Z9B3?-fLF;G=Dz9}357B^AUqMNKk<$X)g&Wfhb&KzV@djgt z(8N@Zl-m<{pgv>&evx*N0c$oOXd7jlGuX=sa!R`V;aTle$-4m9&EFq?B6Aj%J};M4 z`c9D`qrKJUng^ztR z&$XxTIp?gn`(G{I)_1f675Qy?sC(N;^Csl9+Hvxr#5;~`VG8r05cb0h|F)rfffHh^ z0fO7C7t$$EVsX} z?jH&n`(k(&&WGF{9$6DL@8Yxt1H}lMsCf@?x|$c*<>yjsoy8~f7RBXD*8RwA<^3(o zpNI!2N40pjU6HqYHO@tY11A`Qvno+IYn*`3Ll8+HO`o%bfwOyQHT z{Hq21*z4qLhc6~V2^giAqL-C{pQbVQd&D?@MKUE)iNlQ3SP&HC#NG-_Mu>1s%idgjttQ>dz^M06 z@%fZN7BpG%M}|7UB~}JFCu)uMhUMm3cr9gUZp2e|O@EVd=zTdIkX z?i^^2od;n++Nj1|jTWBDtV;gHGZvl-Lv?Q5P|Dn)=2Zk1y5gQ*l-CFw9y17p`uh{W}()#9;o;`Wi6|pzgHm2QI zvgqP^zUj@V{o$!xtbHc+B(J;Aj?q64wDDeynXim0=qL8HVM0}E%5?8#mF!6N%!WJC zW7py9m{gtj6Noi)r|l~>1Jx9|D4FLd)codw!I{x5-o0($Bb=sOEbQMt{-f^FlICsO zwl#F6cWc$P64vTr8Ifx8dM91d?$!GSu-PWeS2yDvt>fnK#$ZyDLAE&VUcH3h%T%^T zq&KrI&2v2kPUTJNf+5?ucXJ~E;-@m-EP2>Q7f8l872BpuzQN%^O$G0t@CtKh=S>rm z=9j59@Co6(5^x1)2h*2p`Q0p5UUkQoF;7AO9qy#Vk=cV!y5gSD*ptheI*69>{XIg9$NMs6xM7pP+7>=sebENJeVVdp zAXL-edP_Et390;E)VB*{jjq5WdLyK+w@XXml2R%w+Av+BiRi%jjK=9-73=9f+sNUk zxH;<)6}-T`+wG_JE)ZGwtM<}KMj1A|kz&qJgn)>~VJW)9(E1W;2!ky=^b)kxuJqPB z@L%t5T4rlS6Nq@_Q)^0UWB$^m{L@eKE+CKCnUd~n^geO!gRF;nV4T>prMGs=r?BYN zjm_7nb+s&+!>M}u^oLOUA|7PNtLv5})f|7`80IKSK8YC+-bifg ze$fj(d3>yEl_`SIHy}hZ)5>7iz}tU$S>YsQS__Wtuy@{6hjxxY5F5&C}X`Pf2`5Jkan2%I8U9F-&q`qHkH;4Q2rLzkLKig$@ctG zjp0X3Evvtu-;-cWfBTj$-RhLl_iUB<qh@j_t(+HDQOevo=Jy38%erw0vGi7+kU_Ip2&VK7Tt@LYtN$_lKi(wH@fQ9W#EwG zoP@S0<;`!i(5C|}x&mj#^5yUb*&SkrVaX*j60)0{VHodLFJX+w=X_)7)V0n3k89+H zA$OVF8Qple)xJgEx(ZmFaq8L5mpC(I60gzoo2^d1`88$u7K(@y@bk(eB{!mXuXrSE z2e6y;0qyUCVp83~V1y`M$cRrRV^h?(!bHBw#XPZARL(yLYvh`q+U^DI+E>+70!-32 zOvJkg@A%6)k1^0A+S+5Wp5%jRs_pV_6-I+@B5Bo6wlPksLvz4E;zUuoxs_q+@q?F> zeccF4oqdMkQ_->^kr5kpy;RkqSk0~pFvyX@Yp}}TS9tnofrUbfIz*GOE65g7p*KG{ z2*<2wR&)5tM^R_DXbi4Q4)vS$fp^}rrR`pj;-R9D;EeOX3CrG_-YF0EQ$@#013l&~ zjBwZkb4Bb#USC*7(xybzob&M{6i+s*kR9dg$h^L-pa9(yEysQtSmLTF1&Y=4KL%G%E)bz`;a zuBv%@^dXQCl;WtJbyN4R55^qw>(*V!&>>Q{!?u>ll3%u6YEe!*@;!PIU`P=T{u2`e zF=V-(ILU3*mYb~a9&We=v**moy+kd>5hb*pB$h8Xuco9Xw#4Nyz1M`6t1K3FN7r@g zH{kzpIJ-)NSEX;qK9V~+H<|ry07#Cy$Rc+wl$S_eGn6rgk0%i!UlqRA3$$4g%`e_p312 zb$nm$$gV#`RsUv`r2MI#kRmAB+hkxF~ zUB9x?)}*?Q?ckQ}kt z$*873y?%b}g9?z)`U5nwT0Ta&t!}9MP}PLx)(dQlEQ8`~;p(GF&N0o%FcPj{T>pw* zF4W4HJ-TBOzs`;G0-=wk1mjpMK*DTh{cs-I2!5z|sPH%3iaS!9meNx%P_wiz;`8#m zFshX=_xp(XaZB!t``X34DZ`~e@-E(kslQoSXDU=7Au^;`b-nCpAk-eQW$kgpe(TMi zr`j{z*4fp2A+j(MP$pY$a9^p448z;)2@~ZC5X|bX#^~+cyV@uPhWA5MG)Y{cLiLkd zJAzQvcEfRsQBWZI@e#f1jj!Og1zJ;=V7wdO*nvmCZ36ioTj^maB zufD)?=Evw7#s?yAl9x+j7VRAtL!KGF+UTs_yvDGq{$?55TQm;&#N+;nDrXw^?q?}` zxYpQhRa9;F!LWM;EoDaESqK8&Xk+v;8|lMJda7A{B?U_5 z1w~it)IKj#%kNhTtCQINdE`yu-BHY4(j0DRm7|LcY~L)w_=LsQ8##zqUkSfzG3BBe zp2J&=_-C}i$tK@KH(bZ9AL|n(S5y>@V=311GuOn)IAPni_s7^-;n}@E!Q00tXd)!4 z^I$F9(XRR~*ovr9G!1UNYV%^q8<@>%jOUwQH;ohbjla^;Im5(4^j85SZ+dw!UWgwA ze~R-U=TmH@-3gNn>;ptaV>HPq37}!9m~SHQGDnlnmMB>0-RCzf^c|Ez4WhdAaQU0? z`{BcO8Dz41BQ!(3wOK~PMNWA=M38TcSibcDh2jVIN0Ut@edfXk9)-I0YJ1eTNmcg6 z17Y9O-UxbCBa_Ld_LiIGh0+4}NCp(Ug)6!UACK2hl8Y9dkt+<7#6fUF#iX~oKCf|2 z^6Io-HLBozAL_{;0TBVhmt7I+YL-DmYhkf|yR;fcKczPRaN$iuP^I_RjWwB#N9p=* ztrvJ$#Zz9nJx__|Wm`-W%{4W9xHljBTuDU15|Vh|<;vq-HF(iAX_rM_bZP=O?9Q;? zbwzoji_Cl>+a~)kWvNCy&*rd^I(ODItLuckbL;I~1*`sxkx%-yG}Mk#3nXD(>FhKF zrAD%w#cJ{X!*tBds=7kYVTPdnf~M>l!Ynhig)OojmUkBIB_`Ah9~E`BGpSnEC2|Qf zt6Ph6mX7F2ohpMu@Oglm0)`*>YB}~5$a4*fLGOBF`@ENSiUEvJTK7eB=18GIh|Zzn zfN_|v;Ib{rYJTq}TI8=-*wMqt1M|O1=FwX;4<7mlF3tk}&q;$RpuT4xic$$Y6rZ$~ zij=ovlm2TGX`^j7B*XjK^tRVy(^N8NEOsMsP0Dp-o5=!6veOctY^6Ot$NIJr z_40lz$+V_x!z72iKVJX2Y@oACUbHfJl z=1`qz);p}z$z<+O){eovr-fQu?kl8ugL@S@wfd;f6NkeK#UEXg*hSubU*;#jF~Mx> z<)RlZ{SO`X(1gBzx>JX`pK-fx%-Qvb6~x*Niz43TQ3I5AO}L2=d3>eb(P-H?px zLf9*kGtiZnEE!9sED9ut4&X7z1i>aD58{;7~*;E7*iK5<=R_Y-}2zpqI; zy@qfKcdP_ufv~HlUY$Nm!YibL82onUNGj&NLQug>K@U7Qn$(+>z}VAZ`UJD&5T%PF z5m|6mxeN0r3q03HW9;8H8NGSYg+#&}74?jLE0dC20Nlz)5&-b~y=Z90J*WO=v@@a1 zcRpKx$miX2^ATb{ua~$_Rghlqf7PwyCvdd`{pmfNsz)=c)4IfJ!^6QS*!v7n;-|PG zIuhY$%#9|CSyY<|R%nNxVC_ z>r?-D4GsCjkYUa}aKazF>5m9h(slz&VJvkDUA>Rw55%1ovTu!3*K7r{0z_e#GD`3N z{lR8l=8o)YVQud#(9R2hJu|*ERmbTK+;#g}{&Um3?%*{<<0mKhqFQyN0EO-{196PA zq^i3C)xH6oV)Qy+v8W-9+EG>7k3?!Lsle_vcQokGi__+p*Z7_>v@sCj#`mK5My-m) zrXmph@F5SP;eI1$>^VM283>XNR{z0Q=wM$X`W3l{1u7sH0#&zALec|aC{Nu@vB&87qu0SO-GZRJr zh1(Q+7Z>1gjalE;mT&O!;pEGC*pX)2OftOZg+<=uV$Zr6Lt@r{QA-7`upYMjtmoP5 zYuA0E;-IzE0GuT*UR@97RW$9=#Kg=?0h0wbp^8slIGN;#N#^_e=QTDJ@v*-n1zQS^ z42GZYy{nV9KYU{!;k)EhwFG+XGEl)m_g zH4tZnEZ7dW44spzV%M#wBXOz5V_Do>Z(i4PwrWRX5M1Mn*}pq7^bvV?_CleQLEhlp z-w7)Af!E$`?g=9TYF*BH^HFcMb@Ke`wP+*>oOzcqz>J3}IN5AiU&9uxkxOm4aYfY} zs=u)l3r$bj5prpjtInNWXJ8S%3E#W|x$=_IQcg?*E9dXuc-i zyy%gBWKPgt3BU5GhUR)SN_BI_+c|d4e=GzzV4hryzn^FFIxStKpG^)QO3?Qt^ef_D zyRG|QGtttSCPU(uLd@V5BGAz0QC@0Cw3jYjYFA2yJ!{glyJZTc;j)fW{kC8UI9W-`%w$iQ;=76xHX^-Kwz?AuwuAxY+i?8qT zPH9hOtw}q;4bIvyq(*`goPTMK@btJ`e?Zl$JtjE-9+?pS4LK5*a1_BlEp6B~ZcE1t zPX{3ze`bF0JFAD0LzV}y9ceR|n@?ca zaf7nyBH)xI>?Ko1XPF|kEiWiyak0ku0J;7xTeeJ?1k2)>gA>z3g(IdENI$bB`M zdB*k@1x~7oKEip~#b7Yg@MzUE9~>m%&Typ8e=K+M-%*&j|K(K(FUEr-*=K+`zW>Kd zOFjb=vmhz?ptV-`##Nu5>~@P+__LPUxC-Y~oTru1ZQaF)R>C9FIx0K|y-KF^BB$07 zOmMhY)8vbo*-lo0zdX7dnfIxWG_$QscJtt-4_DnYfx`Ls)>6*i3L_l=Rn>j(o|B)~ zJ~%X#C*{@wr7urXLgI(IE9(yKv`T$3x5-mBBybc|QEqFch>yK+r zRQsj89BKY%ignJg?tT2nEDA7VPhjub+JPFuf&U>rt<(F*hJSw*$vo$Oe74b)f85Bf zuON4H@U9Mrz@lv(RXFJ$xIpWNP=UhY$5a1%uc8Nz2w@#KCH;aOif2mtA%l>Y0mo~$ ziH`8U?Dh0S)yo_@8?~kGJDN0%R`ebm#HgHt{;*VY^j}C9jH;P5E-fK;79bzhT40Nwn${lIY!jE;&)yr-&UE zX8?8*!F=bpf7lJRo0{5e-kVEs6Re5!Y-WgsAOm4q(eoi|m&4JhXor^-!^_2ntV#S`aWd$XoRD@V%_slb1|Hkz7N)*4b0gq+O^aR zQ`11z`Eo;eX9)jENVbQ~p)Qs0oSxaTHLe~j5_0K;M&Q&?Y_iMz?z`I@v5{C%eQ^A0dfUSd#hZK&!kp zJE1X5>l}yiz@Oe_?$;V$EnajcBBSG=U3X%|g6fy{Z+`8KMhe7VQBlBUUlsJ)1u;MI zAxE6Ui=9kepdZnJdS(KhJZwR~_Wf1{W%UsmKb0Mow=Y%;aykyowljUIpLlsy2uZir zy5-D?>)0xHR@XZuar^N~kk{=4H!7IoX4I5ZpxsH#>(<~aCFUkuUc&5-r9i*cTQK-j_G<> zjA^E_ta9&@=)xmvYP|{gpW?wgF|U15Teo&yr7KB%IJmPW5sVeb!LJs(F5!Ebki5ISBENz+3RypADTSe4Itx+LbFQD zC1cAx8bC;vj4AXce!tOhZAI3CV3_^pj@Ihjp+M5t*2!?u@FJK^tB^q;X3ox)37bdI zH_QG-==|jmuoK9o zw#zhjltiMOTI-Gys1;kh&9O{9Qf&=9+OwX?AfW6k9K0_PBP3!4-|FxW$n4P>3r!cZ zPGVfL!Adi0emrj!I*zY0C)W=K!aSds;n)+Oz6U)lVpU*WU3FKKwvP&WGl_pr)@+td@#-$G)xTOjXaa%OG)ACZSF!vDJ{l`=l0e|(?>h!a>7 zqdppo(-*rtpmiUaMuB4!fcYIfn_^~jFAFN(MuvW|w=jOJM?ckn3ThSeiZdVFt(c3= zOo0k+;049wG}a>xjl5-7)~q2t4(>-BpEPBpoCAv&X7+7=BB@C`2VrAn>5h@4B{OrAwp^EmH&aR!fK3wC|}(IJ7&|rSW{6 z$Kk&!y=7Qq@8ST-8Apq6Hc__`(pq~eYKJqsbHf&zTuDsL2je+(oQwHX>-_X{Yy(d3 zcn5HuB5nuKqAKYtwvc-1A|;t3_tKEFgZK8WA>6yT!k`F<@r0i>oNwxR9h1cChb;q0;pwr0pj1#?NMLaD8QzV@Er|8NMpB1di-$ta)%3xlS@48X}>kF zh-y<|J@o{YVRu~tEn8x8&qRT7O^Ks-(5uDeZ?pX6R;BkAjqz48^onRd%aG8c;nC|$ zTol8;jLmxMjR|GIhBZEv(t0O^M8`Lr#w3Z{tT@L`Ht`V+a-tTi$~!3ZmG;9#(bvuY z$3k5m9)EQ^chpezEk#T!Q9H5+UsxlxaG(p~4lAM3&s6dNnTT)#xO-mR(c4-UlOAuz8^XKxZn>VUS&sM-UkieN>F8v}wI%>}lP5 zmEPC@a%@wVQJ%PON=W=oky7)7>sId*8Fgr!i`EJ0o-0Y0%4j~s#8ji1XSkJ3g$GBn zHzd$KTA$_C*SM)II^GP_7d-bLx2?+YNd!G;mhjfG^WBjnNUFX;NNIDCzkB^?dGzj< z4a`#vnGIASx1b6+o%J~}n^Bm%l~m!|xB5dj7UsGu)*_*9E`sM5cy!sLT@tBmK$UWz z-q{Uc!fmKZ-a5S#U3y76+_$Ju?bxU&{gh(|(7qg@(vYH}t{|1QPfLb5my2|5OAhwD z(&Plg&naQ5Tx+{}gH_@jm7ZW;E9@Y%w3N7+9zX)7{C?Cp7R=mz` z`)Z}OzW{|n$$v_t=hwb+|!+3{eTBtU}i?M8fLVR-+e(qauL;$8BDme5UivTKKw& zvDyA8=!Cd|Y(BF5D}fncZMJzl=~sIm&w?=!NUxLqsw%hM(=*zXr9R1eEhTIrBD|yh zf!R3>H16flJ|fph;h2H03EGS!YrLV#bY$nxdrA83I}sX5-tVi_`>&>SH-&%{H zYYk4g^?@yqhcM6QU=~NF>&R39CPC|ia zw4VeNyW22JUKek?+q^0+AEC2q2X|&P!79w=OTX7S0MMesu{W8vIsI^I^%1 z?yVwgn2gdR|3$f1l_E5?F=8B1-VKw&0Oj`RE9IrIA`>&Guvog2=>F*RH~1d&(oPtJ zP(3BZEqGZ#3tkl4>=IHQeTGK_krUPalMlD;`S;Q2YIqcAnhg?{p!Yc7BmXHzotW6# zqmi8{=6*+*(D$z#%JxlblMw|ViGC@eg}uf;=t4=lU;eD89Bj&^?Zm&LXqws z3F02w8)f=e|6|?DjTPVjlF}Jg3(KIN{xc1L6;7#vwG;}-_cfi@t{tJGxFEuAn*&l- znO$8L>)3(lB@*x%WeM*ZCB# z%(r$Ztfv77g zfAiYwhLp$nE`!}vT6!1qWGc)9{ZDBr-vtOCS3pZX0RLTpFtxqo;|3vaOFU0nfy06& zvlNbd;RqwfPx^DV;;3PFlHu#fT|4SlJ_?yS59stMsZ4T`h4znD1WW$7w|5MTQv)a% zfB;VY^RnCq%zuyZ^Zy@X-yIg!xvf7rp6JO@EHMfOfg~2Jln_A$#9RvzRBTv4Kt(~M ziAt}=1QjV(P!UiO5v52++Mq~pg3>#K^fvT5{N8V4m_6t(Is9>-d!Hv4*)#iF-&*f_ ziyKnfY&$B}H>4?u#a_&RgY%PdIWMXWZ@VuadujWOn#Sc3Q zKGuKp$2mcP$kyOEG3bcH`@oR)3It?%6VBqAYv_EG{+bpyQmg^chZ~sKPRTn@M{y(>rnlfHvPwKI8PVKiErg z%BO4@e>+%xVgD@SlI!tOafrY6{f@hZp-*wuML+{Zkbg*a^$UOozWWuYX~<^+4V--4 zV6mvz=%u|hS@x-YmLzI{yKLK@3)6h1e*XDeFyHgaDsaBFi7%Dsg(y(uWAKsu)#On$ znoaao@>Cj)Z)kTk)5T|;rLeHFtBok<vyAV^QFdM2No z2^oHJX#_Z^fTmsR0)M6DSq;+Q`n zj{W^!{aCiTl*C+;fl&*Yt>O9_`Od4N_&)IRMO-HBJkx@}loOz`ptab4w+ms@1bh~{td%g3 zU@{_ZxA#JFg|33^Y>xdnkm&8k$&P#`wSV#>)5oIbVra@j^^=ALLtO6I#;M$~DX+b; zo?P#qHAmvtlX=M?SuAZaD|tCXZWSqX$@JPDdZJRpJY`#_t#OeKDLc?|-i!Q(wb!-0 zNiB!&$X0WMYU%G%S36Ar@~M+0Ji(ffxSm5yr*ORAS$xl)Qxb>phMd*W+zyg)TAvJ1 z>{d$TzZR(=nu9$)xYb$b*tXN!iH8DZAFv#{b#=o?Rdh7}15>c; zH?%LKqlF=^aUn5}OB}H*V9%)@+>p} zx!C2qoQkW?DFdn6MDhranw6e9;(2-Tx!)$N+1SUa**!YC*!r#8rN6IpH;xWpob%QN zOz2R~ZNq36U+`7G+3yd(uFckP=$hKee;i}KnMv5ZQu^n!MCDg{5BEpw8oq(Z@3V8R z*m^M7xo^uBJUa=C<9YT}oJIE$U-aYke4L7>>-0}#V2~V!aR-dM4#ofE-QPJrWv`44 zWZjFnPT_n5cfq}??3UKxYnb#i4QosYQI-gu)s0h~7lp#wMW3y}$w#>=6(6XqeF4_bR);?WMxr9s1vAckKw5klF1vHt7O^zw}(+;XwA+$nn6Fj<{I6IANHW|GB{c8%J%@}-n z?qepE?oH8zy{0h8c;QOt4dxmq+eX_Ev=UoeiXC$=nDzO5j0m64tDJH3ctvBKclDT< z{`rn@A0b&G%agd>b*bt=hwMg8san^v-#_)BZ2Rr_{WrPdS%-W|!$`qaqp!5QEKXDm z!*H=|nEKpi17(u@F8maiz_~;_4#P_wLzZkFVbd}fSbc4fG29y@2{B{eujq2@ivQej0 ztNN9dCS7`8LYG$X*cP~kpV6b zJ0Ak2EN4-b2Yr01}0nlN4E_xOQ{|FSp=vv#?a| z-cIp`{0UKxw-f!02|kzY@3wcej;56M z5RL1*oc+1-qy03`PTkK^YV!LQaG4bINmd!E#Ywv_wmcaq@;xR+!Umf?NQ7>*wzG^7 z25c;gHGJ>Xy5uMxP-VLnYYJ-2Uw*qhNe9B$ZyUX>jfxuJXCfq}o10yB(q_@(#n}Zg zoyaL@KKVbeM2$AGoMW2gI|0(%m-ulZ98L{}-ineNPL~eQJlwBg+7dvv?>dH?Olseh z^!+KGUG{I<9M0IhgDPuS|LSzS((T@@Mpe zJjA~wyr%7Ooy!g(yvtF<3Z_M5?PQSfe)l$+mzH0EX9av{4crm3e{rRKepB}^5{Z?z zg{tiLELR*d+#(ATW(Xa)FbyGOQ!PfWQ?T41p~D6iwweqE_{Vzv@h^PY#b-8oaO)Yv zK4%Z%2o>Sz^{w2Gf_7eTazX5zI?0X{$P=^5v#$FD3ieahDifd}_fVCU-ve$iExHN@ ztNW{ma!Woo`CcDG?SuPva0F5gI=?c`!jo?&pd z$(Xi$_|&^HsovGf+WMAE>ZQdzkGy#|Z@Vtw<0xZrWtpPT0{sq>w<6>-KKe-;y}9uV z-P;gh&YC2fC*GRSCx6vxsZzAOi}{7~{S?z2wMw5L3WX-oVVO^zi+8-FFdqpEs*4Ok z`k+W6r?14Q+7^*3+@G>yLtP&se%$i;eES=-ukwQgn$KtO<-S!HiKJ|JFf5;-!aC2G zvQOtBP5cmtOMz+8uCM(u#9qT7Ia_|fxe<6Q?p(lO4|_gash?W}s36ypB^Yi#M|KGI z@1|$^#kxef5G!~Y&SWu~EGWIS6D|dapP4z8Wl&G4vniHTs^^T(m?iFuO%D*1)gYOu z2--lV;Ke7@t&25GQn7%AVLDtIMmz1bE==<*wrup`=fbf4rK0L?Y?ABxNviz*pOMJx zmByR;Lt{AQ)nY|Ha_4m@p$~Q$fM%L@A`OANSihe2(T8(^IPwMNyvnmW*mf>k@`z`W z&*uv52v6YeYhWn-nM?*C{Tdw&D5A!== z(r0B+)q9(*stZCO1iLF?dF~aT!9mRC=0|P&3Xgcs*2FCXV7y{j zyWL32`NUz>PDAcT&hYIotTcB6+K-LsIFO`8aUCGXc@~2Q8y=rO{8AK~aYzv2V9J(A zdF+s6>E%1`0v8glAGE(w-Ov5I7*jjfR5y_Hg%m7@vX=a8_eb4$=M`}~NlH20I_+KQ zXBT<^Az^|fcfw+9IIxmHXp-8+8sX9xnKaENgOPcdaK$7EETtWImR8~?LXL=+5-TO8 z(^{(nCUhyoZqpnsWC@H>e?Wc3w9jLH{6Kp;bXy)c(j%izDjYOiPmk9 zY~yaznj_b>nW!Kj^Rbf(EB)tXTC%$aXm~ zM$FHMpl7~6$yRn?gF;Vf9B6at6G@0skFxLOoisGtb$9Me7^@ND4!^}o`o;DGlUg}| zV5I%R%S@?i(P~_5Bl4dZ^W!4c=HKg>AE~w<9g&sN{lDM-b@7is9{hNv%~N3d9_b(F z{$#Y{KThfzPQNc)5Li|x(pnH3S+=T(&m>u6?~gyezBBi`x;N5)?3%k{^Qq;67ynp# z5{`YNBE6Zaaqp1k!-qyadV zIZo3Yty^E~GyW=K6StzgD4Y z-;*GUqDxnrf4XRJ|=kqCyNv~ugAvU2VI1&7`a z+77HWoW@w9?3+EuirWphIq3M#D+cx87vvF|xxMOoQ_}7*EeyqU4Kzi2zoW|(-zgmw z*SH@anl?{yt6$JsOWkL*vN~%k!9HO&o*|gLH$4A}$XI)I>T{Sp7$!eKm20%`d$ISx zDnI&WPp$mWY}qv}f->&0wO<38+_}E~`aT(n!6<{$Z;Q3;ZHpS%X(A%pX|6eCCr`qq zGoS4AV4r_eX8X51jif}y;F+(vA24G_;H#e8m|-kfPD?A7pDkPF@%95qaY1vE(gA&S zCMc!Q#Rad;8^JtIpq%T)pkb98d3=q00Ju*-`v8j8P#*yB2*E8h2Q`O_&&YjJcxIiY z_WI$EBh?}UAn5t^rM11%&DMDN!$xGQ;KQH%c~`dZf$2fH;qq!cdOBhU{8LD^hCLlivzU9~WZX4Gf5Wm*o&`Rsc-;Vz! zIgbUef%wvJ-$WB_J;7KK?p62Tyb_A*&qVXb<4-ufoczaqoi@7t9RWbC_f}dqwrSLe z^T;SI4a;BtD4a0PsrTCP{^>H~S<7ex#Khybx3}ZV&q=^sKH8$i@r$r4f40{ol?2&_ zBq){B{=SGP^-UahOy@|oxM}ijmg_Y_{q-~Y!3FB@H4}R2TL_fh?(#iK9;GMp^729$ z$)7y1fPm4eQ#4jH7*5UkQbVf7=Z%@>;J- zj9;D?p6O0K8`kZvUY7=o*8oB9fsf*8w{;cbK1|M*ur6sGPKLuxUKQJQpNw;rw%ed5 z;v_%tJvk3G$u?AxsH#7{ng1B94fQ!ut;B<~sj2D8H}jV-?Q~vBpn(Ic=;x8~(u>FD zX!|4iYxMS`(<3r;v)zR zjh1Q1yEorfJ?eH;Z7H^=i@P%qxziC6wQ?Zjb=LmgYAudPeQp%f(rlSta0HQ{YjV0$ z+kNhXi5n3a2};e@tz^HIdVA&mczL<>FTM7S9yI8HG3&lMCeRK<%oYsGwkKL=Ys5%w zZ;#d2!+N)rFpYSCp-X=Dk;Y%E}S6S4TYi%Ko@AKCIi4iI6LEIGlH<{>Ce|aP(9vrBxG# zfUrBP3D`Zb+$k8w*L`;kY7e84POY-(49e*CjuV}~bcy8-ihrbeHKP0M#>ZHt{2I5> zS*+u^VM7X)J(e5qyf3}hQu|p3s7>^WbNvu0_NR@F@jpzlO0w&Q7(Ua}Sj;e4IL}5$ zZd_mM+8f#X+!LR~uyB)cnFxpMPH(l?HFm@^?^-f;e@`H__= zKmmQ3VEX%qhf7&%BPUh!LW;2i_g68}(|PVX*|yraPS+P&9K;BJ?`L?I-K>&U<^N!x znh4{kpT1wliJLq5|63Dqc)aEk`vv?A+bZ9Ky>_}<&mWa8gN%;!=YwVA?+EdevuX7D z>)@ZEEkCDGqa%6Yx4#(aLqNuWU8AgDpSkxhD;vZP&lS>6bLYvdFg%14#!ic@eDRoXUme#8mlJ-hOZCR0T(QUFjFz10Oi-Ru55Jwa z*grJwGcI2^Q?vW#;qjzteufrh+4oE`Q}{iKW;iHNSwyQA(Amjm*<*(^7Gy0H%PwmZ zkP+LB*#;xyMZ#B^`pvIX_Rwi=V`uB?G|yT_8=$5?CZ^U0kVNisFtP%F)nBHK0UOzICdtlSF!~fRyFA?%X}MX!{NUJHk!gPP*IE+XN2V0^kx?`b>0@vxTZgkbOwdho<>We~R%Zjvl zW9$)#U&5!B?lIb0o|_fNwp?|}E8oJY7iGDM+dPWkJY%!8JKp>m`rat+4o(|qV$H)^ zK6%=cTn7u$i3xHB(*kocG&CgJcNu4v{d+p;>-_4@FPi)h8FS}_f^uB%GSS#<`*LFw z^wc=ZJ?2bDMM>AYEq;b2#{F#;xtA4=qRveIoHiUA8)N?oTNE98`v6%@)2y|hD!hJ{ z`&tv&+u`)tkFq6;#)DVnqrQj7Z>T6OMW)}kyk=KZzIK(;X$l;Z(YAJD@gS{;$dtBD zTX8dmt|Z*+Nj?-%@5FOj9Q_NUtA(R&Mp>4=+vfS(f;q3JpH=?#bTsueLHgW$Sd;V> z`2AJmoW6Xu1wAQ6GOy~{3o7iOv+moOSQVoiz#qW<32G-CzNo%d<;t_er#w=g zEOA|7I<0x_k+7wRtauP4gVVmgBZd{$SCdD%VgwYPK~qN#JNJcU{2;0o&HbH>nw3Ya zk-FY>O8lD6RkvuLVW06uDzfbnsB&Ff&)HNw*~bo-nEdZ+0`z+a(o~?>#}%vFzFW34 z{_X@k=t`qyF&LHxZS5{O`bA4S+awKf!S1Q2p@UZ_m-#c1_v7=Gj7DLAYn|8^R50v2 zP_l$7@2phv!cG31HdL_ad%>y&d(bpl3Mw6McC@`ZS{ikDJ5^Y)Tp)+=S8?0xyB#ij z<3(hMku<7^5ojD(w&QOPyGmI@q&`*P+e6co%Db{5*IH6N+CU!NgFqxMyxm-_ZZjsG zOhOH;u!LL#=N<36*-?_wlhXL}Kkp8G`aeb@!Z?`cNJ^m}cvGTt(@-iH$q zVdtmjvf+>>Air=G6q(5%ha$6b{Co14+yVrb!w#ewrj7xE=`3A^ks~CTp>-F6fL#$1 zYanm&(}Z%;%L{4ugsQcgCuaf!Oe2pqfT$l+)9q{Q9$MYkt~N}rf{%JN9o)Z zzrIVN&qg)+r+Dxyx(Qg?pbx9#nB6#cg^#yK`8BR zgp^g_ryMS=n|*hlp`jrH*qMf3=txeiYV=S3!6t2N@&Agzx(d)&Foc7PPPTVPn+l$lcC+(trTSJ}xJYZmFKD=I$dzn2$d z{l~(OT#+P$(S4XWu!h178N>SC@Y3u86D|sztTG8#Y&lYm+FV%iDzw(kevM&rBIwT# zdX2{qeqPyA5R;{&r9~=xPmZKju?AXLVdBX<(#cfNhriRkDy>t$!DBtAV4}9|CG6L( zJbKXFx#zcOJQ#+f*Dpht>e_)XgmF!oTfN^cVU8KDk!uYkb5T_7 zZA>S&-Xj&H&K>v+3ib|MMwE3HO^*U|*dy)yd5KLhTt7zga>bl};##UR7>;3u(UtCg zQmJ{cFiH9vV~N3Xy~k)6j;=rB>VazI9$Cg^MkOLjU1Zg*r#la{r-=J$AZmG<}JraZzAN zteq`Rn?PW zvb4lZrwyUDr_`lf3zv4=*z#;bZz2b1Stb?IQuj~Y+|RUY1?r6o-q|JT-FvN&v^U^S z2#-~EDwRu^4)>D5%{ocQ!5u9wY#ig}OxttklYc$z6}cSdW=^p#uPa37J8T~R$!4HLV-6~Z{#%QKa{ zD>Fh^)(P}#M?8fPn6;8$3-$F$Exq0qOguQ=c1-LvYUZp1+)|f83PV*}>E7yyfCxUt zgmy!=b1^fK_o1a>C|)hsU$Rf;yY_aI+)Hz|tE;OA$foO(9yXZ|CSz0&Yu1J=s6k~) zN9*+X3IV7sb(i*VcM$kel6U#F(OM&oHAwmO$(ofeYY;ms;48Xw1;Ei+ z2PnD80lLo6;zX;k#SeDu!~tCFU0`;WsI9f;r@nRGgXQq(OqQ=(>hQ;`Tjpp*08gsY z))Ld7!;fy_dY`qxRy{iPcdS^`padw8ZtIC}Ov!*T1@90u;759RN9!JD{T=4uHfKb7 zV010!g=gIqHmT3?cAkLekyE#E<3_;qBq;hH=9r{+>=A^3=;tXgBkcADyuFs#<8}9J z`j6z27ou)E-r1EC52WB)RqxX#>rr-K@+szw!FqE;`L9`I!ZaoWO$M z`7`PdO#+pB-eHQo&m4(s8cGpYg4prL%Wng*5L{(wI0c|DMgU0^QWygSb!_^>B|pMo z*L0d?5M+h$JJixO+zroSJa(9LtvE<2G=M(#pO3)2dF-Z6JN}tS?zulU=4ZHmNP)AY z-rGH~d_buzb8~YMp1lSsv%IY*BgxF}Dh;occ(ZPJ zxUR(`VU6%myD?CV;vJ$XdOl-;X=PuXwj_+wWTyCJ)o@T~90oI87!E1==PNX=Q?8Pv z-R0N4=`j6o`l>KAORWb3+vQO<{l=qWA<1IKcjp{1HAZU|gj^w8Aqke0&(#DhB5~7B z{zOC-npezI*f7$zPv!zbH{$^d?33B!y6xJ+Q|-IFHm;k!vFTypE{mV3qK9?diSGIg z;8k8+J}`dQ)V;*RqYBn(vURV0I1v$?U=@3-?8G{!)2&TZ)pMU3w3syB|Bg@Qtq_K( z{tjmq`;8k@U4Tm$P@&+wZQgr=?cAv5NL%WjgVHf|PXpJJ=h(wZ+-5}<3)!VTIyp{g zAa6%KY6TJWp0ULiJ=@6#XFQBFERT^)H0jOvGg?trR#yRov!I}$4?^|PHbdQTBOSq9 zll!1KNY(KPRqzlRKB8+l{U5tKhk0%N=on)m&?^`&4ArJEo1V8n}eN*Pva(;r}k~yQ>y)+=xIwt3>v7iiZV zzkCN;km>j6EY(+)qxo*BO9m=#MV3k$9eps29?$S!Lo_F9hQoz}P$4xFNFMF%XBRw$ z#-|724}2_>uu?ddGmP5}BLe#|Drcs|XeU4S+fHW*f;eTM85i||&)=r|R=#ChsGrfp z#l+Jq5IKwLAuF6nmS~i)+yedLV3#2E=;fUk%*>V>(MUS6efn}vBUHLt?T z$_&9cp|hKv%T9TqpiNy&Z?>E4=IhN`kw0Y)OF^LVWropYjBNYG&W5+GHOql{;%ei> zS0DIhFzXhWd>CrY(zy@)_s!2*16!fZwD_Z%_E?9mQY7V zr)9Wqn!BR#O2gz@a0L~aEFtpvceeZP?dMwh9s2{@3ikW+c`h8o@-Q=hxsg)IcC4bL zFUC*3xEYKs`7qQ5JgK39N!E*7_l`ZB+`MhyodwS3AW5Lt8+#nIR>yvPB((gi8Mk?n zDs%gGeUp)89=G9Txn=fMe%n@ceD88H)i&n?)uYAKLwtRgOf<9>&nZ|MOAeyKv7Eu% z&yVu<^P4xha5yo;D6NVeulytUydWU**Eo0ea_LtB?v$?5uZ||8-u$*5H#oMGCk+=G z8nVemiPqyt+-6PUj}XPpt8uvT44~^_>w!u@lyR{)WgO|G*@@enTEmFxcfJx0wj0WV zV;_!rs{f`1);?Becq^Q|O|b^sw>S|my%2)`LJ(+%;_jW}DU;S$tc9e{QFdBdNvL+X zbZr1;_+3>PPHDYbTXNBF0-tq#7@zt@*ckt-)K z`R9`t8bcbQw4>v><~p6iW3J|Jg<(;iFteTqyC|?2432x84;qtHbNZ57GLErR8y!oO zC&<55s`SVfF8z2xNsH*oUCn#B8Dd5&j*V5S+ye}`6_5pPFr~_0 zE#21~`Bpv3Afrf7*34kBd4)--Ro4L$JUY37oIbwn3*qrmJdz`c|Em~6!e$V^)=@Pk zU^YU0NZon=09vZw)*?@L3W5_%wqGEg7=TxxZ(i)LAn$5lJ%R&pj;HATGEww4cBilL z;TEe80#jWBg_~l#r6} zP;Z%@8E%ItcXp~^xU>o;IV{{Eb@e~d*HV~YjE|q9n+%H6wm=BEiqRVrcjNwIM%SS+ z_HZlfK0a{c&3l23_XU-(t}MyKx4&pAe&KMVJsL~-tdPWw)&mhp!Dz{q1y(skxO@lb zTt0^zx`1iBQ)3baT)jkh@2pzXLhod`E66igzy`~Yj}Ef(><*SWR?c4kgN}Wtb-wg0 z&Iq^QFf!RWEH^h`-Z=Zi$h)yd)LMYxieh5zh^2{pgBRbJ9! zJ6Q}j`{T(!0>7eRO-m+f9riUDfbiEKq zc}k|PDDxfn_%Z}iqc-&)&h}ilS~^5Sz08q}1{Ots@`L5z@PMML&XJk?&y&(kE6b~e zJ&OY~$`-G)vw3iior1yb745rZB-ONr6l(*HSUQ2%o&FkikY~W{e*PD~Xo=qp63y7r zIi46{mtV9nJaMG5WbEOkDS%bAO{;on?|UEYUI}#}T#N~T_O@~U<84Q*(aX48s$jkU z7ggag4ahi3W&Y#kHfuj>{NU@=T8s2M6XPQIB0<|})U*mv?E6YUk|I~whfFetlhS*W zRP4TG4wV&$nX7OH?A;`9iIOjy>|3XRm09R|=8usMV`mG%%G0#WDH^Y8$(_i7D#4jr z!v&{xV)Cti^0%p*ADWkakGGSH>i4$BTGmQ6K=3&dlB=t1&GC*`gQl!q8U|GGaIMd7QK;W{ZrSgB$CgMod0caAZ5Y?J>QBO=NW*ViHcUDwD^v**Anlico) z`9(zs%cf_-o{V*0KR9C|6|v<*00duiS0isv$I*VbIqBxQs?YHpaK;ASg=D5#-BWc~SNSjz>=<$5tRE#~vBD@rY70{n#1=PW0M)2)d*|2Q)SiPw43a2Sc$7`6V zeu**!M0W)ITJuT`iIpskufxIOi@vb7`I*=#sy-ZoD z@@&#Q-PCZg_?I8}X7N!iEO04W3tO_65)2bO=F&LLUl1SYEEyg}q2pDuS^-^ByXXEo zAW`+koaNrO`DAW+Xg~oHG_!zX^)c*f&e|f>mi~@xWpS5&FSE?5uN@tywrGr`Jru&# zx6AXU`Eqlhk`e!<@m43ct!3rqw`8Vgb;2VU1f4xdcrvKBVDpYLnOWh2VPQrRZ3)$VK zo7Su~`glh1R|5I;8C_t_obd)3co*17SPxvm1OpT$56?fF=arAaE01Bd1z4mWxz4`$ z%6FyP4y61Jp)lwYdV)^@@3DIu@c>i@&`C9Sdx&KzN1&&1Xd>^s6^`c&7G2u8~L$~!W* z+uM)!LZ!VoM7P})A=0~tn-oDJdL;T8qr`df3!^x|fCO69=&zEs^jD>IwbLI@;?Zr- z2(U-6nV{C9J5jad^9-ITw&Dvl-iPgcWtQ=QO*bCzyIKr$H*h5HL|uHOJvQ72UWSYg z7mPiwNSW4tI1{->h~qg0o8p;RBVyv1NOEb0=T}hmCaVh%VCgTZlP6D@TwAu0{nHAb zo!W}xf;>BEE0DPffo`OUo)+$sKGnF^2aB1<+P|TJaojC?+ZE93*4Q*F zv+_qeu<6!>eWKK+F+FoNZnaBtu+?LZJw{`{Pv(JX&N@jm{9X9wO$b@m-rhc{3W*b} z^d)ZDMVDT`BlQkC?zj}N$ZnYr`taOqNoV)-@}s8}kG#-IQOT2!{=g?bI9#j^{479_ zaqQXWbjhT%QJev$n;?)@>wzSX_@%Sgvz+nFO`lW8W_^uT9P^53=g#gNhw<0+?v>Kr z5acy5U&fM`IW~+$P_DS>f%dRPGg&ZvZrJK#-b|IGBP$ckCHO zte`9Hk`H|%6r+NLnJK!6h)8Yf3&66qiH1Hg7?*U%F3m~BcKELI;EXo<8I>64K3L!X_?i;F^e~EA0BcLx z+3@5J+*PvIQ`!)wVa1}Ic+FwE5%NE*UG_&CvQi1PK`znCR8Md=@B&teIWrFRX4W?P zgSTC&21k{hn4u^lWfa!Cr+zqy*QPM}R$))_&ksZOJYW(s=sey#|F zVPX7WN`88m{rLP1LkO4chx->q2;AQm)SeIb=Gr5=cVZveHhQs42PzMd$0iG}g=^t) z22GiENJ4*R*&h>U9DC87D7}`K$5;PzOVjmXjU_bPoFVL#+=2>Ebf_NMX4>0CSS5QX z;Xa zsyYW1k~eS55vk-E>AcTx&Ft}t8An{CxyoSDj1!~qp6UG87WL2gV*I=;bDf2ukm`mE) z#>NJk;mapBF56$wF1`sd00qNagxPW2#s%hptqLHzRM+S3`Hy$Uuss3vd9z&d5mK5FVV z#&(k&cb0l=-&zE6_yBwzUnABGh$zs7sZy9(qQ!|H*M?fQmj&j4O8kHWMzddnygvVF zzNttA!yzoFgN&|NyZF6V`u}|KsY1wltr)(la=R$RRY>qWwyRzPCB9QS`C1>&O3vB# zNR;}4KgFF{)ji(OfAtzv#cwc*sdWs@nU6>gxF8|o>XIMcd}X2uQ2yUezK3CX#7d$> z@jMs|QrATcl4;C=1k%x~Qrt-%5!kVq7{X{z#LQ(qa2Gew4#*h##om3-3)37X)RO$f z_q!!)YcAa~SKEc7LFZsP#7_e9said0KI3u=z05T&r#bzk+1qZMCX9HYD*A%iZ_McR z?=jeXpr;obX`8RBX$wNRz!vwUcG$KYoBg{L zg$we=GY>uPy1tPystRM1Hm-xm=4&PKW`=95-g}0bn=3RuT;CzziPf8$;*>{(tAN3a zSTKzGEgGbJ3xHF+KSTSFxD?Vgd6FRWw_x?Bqibcz3p&L55)OaVHX(=7#V9(#Tcq(oD$i865 ztqxuhqyTRFde-w7k7Ijs$>Q-K#WCVpu{+crW2;wx?*>A8!bk;DsLIUCR$B9)7W(o5 zzy(0|j`=*yI$GsB*{bc95`QH81aNr_l)doj(dN{O@ZNm{w2oVf&8_NDXOCC(7 zO?EH9g|FC+82VW)us);23k(^ za8!BN1x)d$n(-T^uxl;*Cb8*HY=yzvIm&v9Tb;|TJMT&vYeu+zugXg;vF+@Aubm9L z#}-Ex3~fsW;I(c#ZA*s6V2uSJe5KFdJ>*?DC$dcvtMkkVRody<5xJoYyC6%l0`&(L{f7F@;TgNj{`zx z-xL{-0dA#%BfW5~+HJ%$Kf0W%Y26ON>&o8#en{`o<*outel~T^Cw~!xas8IoD%$@! zYaV`uolSpfXiJegcrAKm5f&W8@BDe0{nfF}6YGeQ$^U+{e%Ww|Hv7Fy{Eyb=y>M`8 zfzxHe%m^_$c=M8Vu-Z)h6P@PZohc-vA3`P}>d?yA8Nydy`%bk_eq~v##tF^`N zEa!;}75K@n4<-r5%#lhcKH-n4yN1Z`Qpwg^P2+R;Lcpe2%i_4fAWJ0NTxINo|B37; zJ3u&SLEH7X55cS~)dGQZg1Z{12ZmkoK|LY^Xa>y&}*#*u4xONf2#EJ%3%9IB0qDq(OxX5TKdd^2a<@#``Ppi&fBr|^zp7HZz#Dg z#bm_KL#ML?Bk#dWO2n z$;{BS)bq@)uZ{Oio-tC|{VvURO3d+8u9VQ_j0%kR`jgV$4)ba9yt3M$RHkPG4!9 zZ>pQyJ)e=IP}n8#MM(3!>)I4HkJ+mY&Vv}|R>4N6+TgF(_Y-_UNCv5VP&2P#4r6B| z`m+%XuCyn&K!5$$3`S6UCLDx>t-D@BJ~E=w=`>9z51i>O}v(SO^n$IVo|RvAD$H!WhI6?4nke zZ0j8TF!PJGKYoVM&eEd#*a=|%XYMJdbQVt`GA+A(F9OeHFNCd@Ud7WTRo ztJ-Kfr^}CjHh6YvdIjW4Y!a)X3@Wdk}i;*9*=<>gb^aT+i90Kjl&Vo&H*Z+4Y8) zK7^b5gWVUJ^&tStm@XcA{B)z7_GrbEF&SP|sHE_#SxOXN(yeVTDUo_Ts=r zz09$bLZeQH-i9I&XbO5m zz$BX>vP2Jj#d7_1EKVhb$hZCNo0ZX$@iub(AE(W6PD~SljmHB}o+|sxLq70DI8uCB z`F1^(Yt!b)N#N0u(ExWGtRkHSWyCBm|NWc)vzlqSAv^gHGxh^t-q;)d$325As>Z8A z)lNjx-0@*n1$N`qDd{&N-R9Uetqc19X_MfdW^7;2QvGXcC)0j{L3pr%E1Q@sax?lm z*IEw5u{bY+zyH7-I^f*i<}4K)yXv{GB9UTE{+u@KX~@3Xm*wFRTT6ffuvFKYP768? zdO@gM&$uQb$*w)%=tZ@y6u00O>u+!H?M}PENEwAyOD8)GYuD^Q`BRU>35-eduo&OD|5NaX82cOF_$1!Ze8es}d z77|OTbvqrG-%=d*0*2;daciv2LvHPMLRx?aV0jgu`SjRul)+WvZV@jP@cE;E+Puxq ztZp9b3t?e{Ew#d4|DpGIVMyvp8AI-$n#7Plo3k#oUZWGT-n%d~K6Q;B?Mo4vO5Z+L zeG%4&?i}~{67@OfDEHP0MBa4DP#VC)2R;@9A0zgy^SM_S$W$FJbG&+4L|A;ZLB_X$ zFg6$h-%$NJjToHtE`sRk{TsbK69^b%k@r556mEuCF4#!V}LbMf->tEm` z4Z56kDv=$P5k`FT3C<$jt>Z;OmtLMe<~rVJ?|LbB;|t!st2A~^hce&$E!YUa|}MAp~D=FA|YAIYIncz-}j^ z1LyBPTeF{5v*8Sd0tjfUoUhRPsKju544!Ybt3R`4x?T?J5g?L4uOGW3*A8QpNcj;;D)x?cW%^LNTx-=`sV;0q<4IJD?kD0F|bU|W2DuBthTcrtpT&*aVY>&*|t4GcVun;`E+5 z`teyr%LY!tS{9?kRPZ2Xq0e$gx-4! zqyN;;WK8MYdHF>DmWE)aM<>s9_~|Zyfx|+bxS(y#NQJ+OzP)t*MF*b^Y4 zl$NKBXEERY0Y@OvE0|M3C&uR8<=BBn!p-m#5?7nV9p)tFep0@eZXSPSojBG$2&0H6OBpsP5Mn=RGUY>G_{C54S!9kRxBOCxwj{_u|gJ3Nvw!^ut}Fb81loWX$^5+5c& zsbRkC>vey7xjA|!2C1{2LvF`sKez=m3-Q_7fg(VjJ>Uzs+kw&k?krYfr@ex;d7ifG zRc&}>VGDDTh@AgYM2`C;$hw5b1$K^)Z>YX^%Vp`(&A**{9Oj$8YUj7Bt}UPb>+OGU z-Me?sT!X@vYTFS#{m3&xai)3;zT0zj&(gVDB_)5@qo3{&__%G=`LJKV{m+iyaW8;0V0_W!5HN(RpP|A^CY6XnCW^j{D_rqW^c2f%*Kmc z+X@FmsP>cyIrZ0ZsjOhA(TOJ#npM++wK~ zAH8Kdt-bO2i(n_%#-2=oJ9G*pCpQCU1OQgNQ}yh?DoOY$7swhUd`Ia@3uwQN(IvXA zgL`}t?DzyZ?|GP5o{e{VluEZ@8cI_J3b+C?HL7_mHE^$5@n7ab?jEB2655%x#|SgD zjDB{IRe~YI33>y=V-~dYgf)Nk<7k;3y4}YwFKdp7L)^<9CBNRu-*Ww+Z~lq|(mwY! z7Rz^S&i0WJpDt~W-ugqJb)cLY+U;LtWrY!BLf6Jg-LM6>r z8-@v#F{-7yeQ&O8+77fd>6nbj^RG#vQMnvOVP-no1_6B9!I0fmb{(Kv+yZ~vl3!rc zK3uObIH7&k*9*FI=H+V%uieUjJ`Ey{0$XN9LVjgsLZN+B=wn(Hr5nBR8epklT!xA8$ax5;wVTTZ==)6Trt; zGA3^TztB9+2)A%D9G=)@Og<2{^#39I>6pylJBxkGnd7S#5Z=vufUQk`eJPgx1^$b&3$@V#1(d`lSN>g6L*Aq*aP^| zgLq)7_chuLA)>$w8p{m*D1*yrAD?82U7IJ{cAbaRS0!dWl%<{1Y3E^@d>#vDi~azA zoyyH45YrN8g(o><1Jw#-f{yrx6oX|1B3mjHh4(|LAOIiDM(zs*7o)xPv;_#`U59j_ z+5!(4(YYv!rw@8U3l8tyA-MD+&L>L3f;ONTmW0NMx~euO^*lN><2FwL>ufV zPQPJN2NTE+J(Vn;Ny<3EXUw2tC|N|Oa*uV#dDhpEJQndq_^TTyti$|#Bk<67+p@jo zWsl4`7f{^l2MdrZP#tF+61ng0gtNNqx+!j2j@TKth$?A9EcT~S^ zdv8}w{;LINK0Q3JvX2P~?DtR^^$Mvp#GsZw&3$l>2Qf z1A&64RiMHMwP4ph$-(Sc0%h$@=^rm!C1_e{XWm|k!lGeyIjE5<@Wj8p@rJV5e-?<{;{YqK`dI=WgkMmF8u4?Y+sH&^B zj?i=)>S@?EuI+XwQCnFD7KowMc&F(Lfp){O=c?>WrQzX5E1nmUrDwR&LtA1KC5%}0 zlL_;dY=4`tDB=}3;}tx$;jHRM7M{&~(?wIbj5<8`b+sGT+{b5Zj*G25?{lpj%6NmZ zz1Ve%u01?=iFt)&`E*8&Jp!@u2E7U4=D2P?lLOIOUBvsx_Q{U*?Y>Z`-i;Zk$^dXg zLqQpg>AKsDjhkIEz|)Y~xWfr%N`CvSCQHq4y6}MB;s4@A>o?<8+Mt)&*Ii|wPVhdN zHovs-$*sC{H@ja@b0&XI8&8`y&7`BsT5)y0ZNK{iJC=*&2{`xlfpxS}7DAg$K} zQlqnwS;>!j^UuNOwUOnLw9)w_|k+ua(XQe`_Fc!d1``cagB-Z7)7eDK{Cc9-qIovM2U z@Aq5z-lAQ8(_b)|%wCwp*}f_Fg~#`%uB z(-m);zkpoCgyf_BS0!_r9?$z96kX>ic=N*Y0aEguaP_OQJiPSGng@4p7 zX~bSM8ney$9=-<Gk1b|xAKn#nfYz1#OH?-VMJ%GIl-GZV(odz@KH1J_EoTAQsNUV8vv$1Tg+4UJpdLpZBiCp%;?XKee%S`L{Ms#l}uY4S36Ej|jD zYrcqRLh=lXuDTzw0q`L<^f>pV128BJD}bFuQ1A3hQwx9fEE;xP@?Z3Z3loNN*h%Y( zPnJs{51sV~MksXg`0O1-sg7?ji94M?VUKLPR+oyH1xxdK1c`?>K49t0O<$e@$7>#o z0k7k<+ULS;b&kD+hq*Etc*Z%!Zer>Sdm3}Daa;tIJB>1&2NGra71|@vXH5P)OfWo} zurbjb2#w5lg=Zevj*p?&t3vm^=4gzB-g)f5uGu0s1P`wE>fEWD)s;Zg{A2{=Xxj+} zr>o)cP^>-zRP!K6^6 zlkM*PfnfNg0--)q#+au^hYugl57bJOE5ZcR?6s0A`7X3M@Aap9{>;{`y$bz@k6IP% z*kSZ8>SV~P^Q>oX_Hv(9dUhXjT#Ax57YT(lx4X>KPD6KR9fu64y2OzVizkd$GEKQ!^vwX}94)@(+82B0so%_uIrsb&YV+}Ie z#g}GhqOupkY!dpz9iA{tv)}0{y%t|K7T~KBB+q=HY@~3+#c{*`S$&K4ZvPV75AsTJq)9sUI=dpz zx6f+LuqQL4z`F-dj{lWaDZpYh~lMb4g$A6_Stb-R=KbT?=hQ{%Ym; zh`pbA)RSiiwX59hzLV{UQb#K>`Lo%2r0)>(dvlwS@*^^BR!xsg+KLn1QPD&4Prp?? zq;V&r^!@w!aO~V)u)LlaU_*hp=F=Q!NSh#Gk1o23_f;3>Lv&kdmgYjD?k{)Ljj#Dy za%84fIxh3+2uZm!1;;prkVy7>V(nBExTC6S0G6S;iFFGy;utDRQQ3H#U&bl)l%Y1w z1qHJEt*n%cFZ@KCWsX7o?lIh>KhNX>-2Pmc^?z!OsL8dbFA3)jg9>X6ar0p|?>Zvm z3^DQgF-Ji_7*a>=$NJc4;g+_#EZ*?5>!#tRlkz2+Yfx55QTjB$m-C!*husHwX1A9K z$h!(1Tx)z)0hG`vzWLA)?T%@B-1Ph)Pz(mn$qXZ#8m_uK)1-&V(XX)ocqb#yS9F)t z2CX3{!C#NX@oYdjVWxBB<_dxjn;>;+xVJ#ii|!3~7<|wOA9?AYH2LRMeq-br^`w!| z7c>WArxbFV!P>#xd5R}1_cV977FSyv7x7(i1*JMBJ@66q(FdWRJB_!rHwk29e43C6&SmYG=Nm``|VO)_qYTX_g3Ml4e zf(iWCYA=g;2hJ$JMKvsKC80z|4!}!!8|Mv0Y>-q4^Gt$QxBT`w07;6(1N#+S5G}a_73RzmDeLWd76>W%$ zR7gU*QfVJiqCKVcv~N!PdaBbozw3Q6o%wjoKDW``J_Ko-+x{7z9263Fur+iuv-hnIZ_p!MrsYp+qU_oe%tFu`I3-DH5x4m z4V5Qh8&&5)z;dBv>itvbloO}3#X`^Q8{E7$)i3Xq_h%C$`QSKXI2^s?T! zAOR5MEyCd_IVBvy1W*{U;Tfe)r@>-YU%0LWn)2qZ4oG}%?Rpw=%X(VEEdyP`cSeY`Pt)5X0#`#J~^ z`Ha%DT*Y3GOQ+#}Pq)L%9*MmOR^{WDE_s;m2L8SDF150w;E{^U45<~{%g@+ zL#vGR-pS9)NEQCD_-owt;ia-_2C`eaqOEnwFEiiH!DOU=U1s@SqLOfAuPOhZ-md98 zGQ6S#&%39jV+ZAq%?K|(H!HWWatVps%Uqkoj03V5oMDCDq3A-v2NnlSaotvSZ-n?y zaQrQw7*W;G&?uL%@UriICEX1(uSv)qaw=87ckLmVyNp@m8!_3l`#K~Nw6DXb(|Rrc z7ot6z_lmMvNNctzMHL|LIQpS-Y#Hcwq4avSY=seJ%iZemmqGAMUdl3mrj+m zPMf*t-vC+E@Z|f+lOH57bMHr#;)3li0b2y?@pmmF%3NDEd#!11dGz-?Q=8$A7dTSA z9-x!nxvx-Go1e-do?2M2O;K&!4am?635#Emxyl8gMs*Wj)1@6ea33JFT93s~LZyjv zOR(AH9i4q2!;InrhpX4?^w;+6DlWTYMtS6-f_2b;wJ{p{93igLdnfwMN9fa*QaJ$}53{4UL1VG@={22%$DQI%b#1S%RWAp(Ezy12>8 zG}+Lwu)$r@YP^x3U>nkRn={(B^UdJHyD#?dBK5ol>IATq7HCjTW>@F?xI(Wx@(h2= ztT^bz1`TA(qwO*TRDtzojpe*W=@kd=`y+P+fXn=ti2PXaQkOv5z&}~vCz!tu*&B6L zB{ly(mxP}e#^jcH6Sx>9(t}097)xeCN&^JMmD%jJ+!RRPH6=m0@ewE~;(9TZh(*iv zjZ7aXs7N)@?g*Sj88xX>g*f<=8gmRs+})a7P2NA4S6eGH(&%o0@Nz~lcWD(U$(x`Y ziV(NjT<@&FVGkg84Xl~MRc|g+&7Q!x!-~D=%;ja`BiZly?^+}qg9phz$7As7zRhSw9;J4Ng%mv#zdjVH8c!>c;^FDUeehJ9kpSU{|@wov0|@))16Gn6tMO5{E)8M(-^b2iI;9T5w!%Sjn9L zp&s{gKT#5)0#_Rp!0rZhPd_R?&VwB<37c}zb zvtsvdhJ@1jL#e$?UF+%p9X~ITLidU}!)r13ZR?DA3J5RofG_&)Uiz}XzGM4J%sj4+ z4QS6Xv)j>m^8LNYxN$Ja2b2fy3YOs`Ll9Cp8*C>=-%Sx&9 z)W&SGZU*ha5ne<)$hyOyl!-;yM<*>c8SD3pi}#*@c7Ut4b$-c=ier4=5Co2Eo5sft z-g?UWeu2wN|F$T7z+vy16RWZLaKifo*f`{6k4GtrP5<-P8vasG96l&O!-G>HV`s0# zR+x~!fDkLLcct9co?!0gVY@zeQjYeAvWkufHXwngC$7&!gI(~hWLv$5tbUK}z>D~4 zLH|Sj;=OMqlbgKS>ylyz$<0}`l#3%_T$&}C zv)sHb+aO%Su=-LaIB-rmKVl?f_|Jl-_&;wn0=Q)IHyK-S>y7S1Ga~7UH!qn%YNfHoSQ^Jxy~6T>R$S# z2BxHEYhy~G{-Xv-=m_!leFWHDchW!p70h*WuoV`FoE69Gx1c;bITQ?A2hdCq_CkTJ zBRCt=yoagNXt>V2aF{$-yPSuFG%@)Vy*WK|1Bm8ormVW}r_Nl=kjWT?f74e-?G|i2 zW=#|P*2jSKGsHHT>31fz1%+B~p=wi-D2`EZ%C0xz%O0(C%=b;J*X1k^lCifEsLT}Y zd*P%c4b!}G5mGmPg5o;vb`HN8h&H1KYDhyV( zU@S`*Emm#%O+_?6`C&=fyy%Sa?sll;BwsCn$kHT0I&;yb?dT~<>k+AgT}$JtBX)+o z{#tAy`s;PqAMtrN_-I>$sAJ#<%WCbqUzug0RgTn^P?S4xB4)8wh2_vGYAsxg8bfZ#Q&D&I@}cEjyMFgyH%>H}D$L#8Z80MZx#O)dg|*_H>{Ab(MuPh zZNL0CNJ>|CGSPD@%a@n#vBK7I4wk|I9o$S6;XN`Ru-Kkri_@vEH_%$5=wlC*aMnx; zy_d9JMtwbjVsq@Jwffsuz#r%SCGY|)tc5^@Z-d8cDv~Z7Ef*=|<{9c&5%6q#8%7C} z<9@K}av){nBX`Ok8tuHPivPy*hdLx&cA+$*oa)6d1*@`$@mLf^{7&C^&vRFTh^iwb z@kVN10Sw4R9dYjuD9})=Tt*Qb5>3SCi){8eeBs1 zxK0>0eGIdypx$VyIdfdY0{b&O?r&H^&eyK}_x%N%j~~*YC(sE&cSP`@%X5xt}(j~eh(0wobcgbYHN8&V7zVNu^(h0LTeG1 zvResM7^(?*Hih;tkhC2BX6jQ_Ha2f{qjXqsP-xnxZ=~P+NuT(HT2v*UZ~sH1v0q^T z5-Oh!&2JC;u~N03Su^=mPA!J~N!4dZo%08tW7Ww|7ECo@55f*TU)s7qED_272}*)8Hr(^VvdEC`lmdMh{1 zoY9yAuXoW}C;J4EmlHVqjQ0jfFE^!!oah03#gW=#cf8~sS^gkB!&h&|QvzPSt9 zi#f4vLCgZtcCl3^A?~lKW>DwWPu6JN9ev~9s8GdBj-xo^GV9c@yM5ZRqd@H;Nq?PA zc`^$qO478hPBM^Ltut^hf+xAiG|5oHZLZt&>EztWN>tg&A@BZXSxwQZx6qU2c3NA! z|CO}Y8!W)$>N2K)9JE6pMwO-lDthfD=K14{`6F;}s~l?z>htio&Rt%tbm0i*58HKk z7h9k*vstQ!$}x#+sQjVCso1vdt^Xb$R4DInG|CCJ@y-uRDw1R*Q@awr!2h%63w>|y zM6{~vn+4ns%QqE5kOJgQJO%)ZJ}P^k@bJ+0X60!ky8x*qh*+eP%9J@kTI>IDFY z->nCQQINuMPX2@MI#1@d-YaXmRA3TN0YNnK;EjA&Ga4N75VscrUi<%h|}a^WtE9VZC?iOwlss6b-$V9=)NwKF8+`iIv6w^60{`{bt3&ZrTSQUUaT{5az;^xsf`8;+s9 zo12^MoLKd2iMhj7ydi4w(a5ucMQD$t4#8i{+cD{}OM$sWN>T7uLhe8A_Ffvg5Fvfb0v z!?h<(-Ztf8^E;b9>-4#`A-k5FPQNV}zf5_T*dtJGZT5k{6LyA46zcA*V^B+DAKkH4 zzR6`yQx1;~3k1ACumIIaYmz9206d0x12EVj98lb2e1R`a)Yu%!*b#=kwDJF@KzG_3 zVjQ=Lx*&%1CxkKoWOFJPwc-a5Qry*`=I`Zk`9Y9{$IxYv8iQae#4-^$Z&8yya zfi7h2(59$aMZT>}2ZRuCn0^1|7QY}v+w9xe3l-10#_zNBVzQ%;&xw;AUcGaUv;&mT z2b{)*z>+%5&3Q$VW!CRdShj_sj^*^MEMiVN0&%^PR`}7T?)e|B7}y^W(%>Zh%HI`{lg zpmla3O#Rk$)#|cdD+~fhESa%;$y;L~CPvq(0T|q%-3C6!8{P>#?}lS_wuW2V5~?4q zZgOd(568vknB|-i#7hX-&o85Ko~HZ}>xv}>tS-|y6FnO|+a%yU9lU2Gl9Cnim|_7O zGOmjM@jo4B^DZp>j-0g8OyDGiDXl$lUs*X>)sr{D;t-@nuz##h;p`7+?AJ*Dc_DP$ zOuZSlZ3^9~1CI{bv`Vy}6zHDm5$vJpkURC+R=kl$10^lRyp+_6TS0~>Vlo?U7-#(g zk$nP_pz4|Wv)#Tr)y&{Nz5dFa$YTj2cOJQYg7=4orRACG%wAY%$@7%Whd?N;j5CyG z{-OvqU}S$N6*XZUDJ`pLuV6NQd`w_EyFrX}YbUw6>SvkOs2`v4VNx%D zLpEj;8uZxu+lTexMt-)sHy%W0LT$4RpELDSXccGMx?gI{GGg4f%=!SqrC7}TYcqR` z&TVYOZo%HF`%u9pTK>pI`?1B<;dvsm)%sG^4S^CeYUA}89AK zDK+0U#9?;%6V4q3CgetXwuwQrzy2{f_;FL8}0D9Dz~NMSX$ zL#yM3NrhRVQq$v6pxSyiuUhY2B(&IEyQUmyJIETYVE&TwnaNZuK~CDUWme?>0rpdW zf{DXCaCO2>(l=MTtU?yKctAuZ;$c?Mj z=cc|7jivs5whA9q*C4S>N?`?K2F{ zI9Rj=oDY**+j zQ8O@#^u-*qXxkwte?GG6;trnBwGZk_n@&uho?A_5GxN1C{Q?4T^7JgV>(#<9HR$;n zHQ(C|e=MiSTzLI^iDDLo5LwT?Fg%je0!fqz4ic-e|3-sdd86+&YNV!KE23s66RPLa z1LV%s_Lg8>-mLV=8ARxkPoL(40I5~dDuX?HEcP4!ucF{c?w8F%M5+D&RsPY-v^rmT zYq?nqQy1e+LgELZ=>>>NRjR!5+2F|3U!5flJkiOn^={w0i4nt&jCS^bK__(Kt7w}; z|K{?N`qL=)R*7QzwO?mbG&;DS%CNgi*hZCO;maET)fOyOyG)fZ5U2jcC%WtO{$>*& zy`JgP<^<)poLEpF@vLM;fsgRQ}_i(obS-xLIa1XLMBYd14*keE*yMvUK)y8A2QP2T zEu1KFePN=BhD7Y8tIN7i|`_3U_a-K`ypj|qHOjCM|!NN1hos&j8cj2y_g zpN^9);*~O39Xz<&w*Jb@U0{hITuolwdmC5n@>-;6=X&`cd2hYaqWT4+mC|4luJ28L!?1X%eplK^_8G`fqd_(jk8*QU({2k}v z_+c>ZCBGO{gvdblN@%U1GZs~y!7{3K2QD98HByr4Q$pT_W;4@i63@M_7ml-n?&d;9priBRJ4+hko zuD(Ry!R|HVriX`rsHIfZ8&qiB%8vT@{7jdGQneLAZ%;JRjx^clog>qmlyNNb*nkHmOI4MKdMU}3LqAqio?TWqq1_2%EKT_hTZ!#w zR7!X9PMru2~GuB9aHn)DJ zL54=!PpA*tXHiOPT6H(|Zm)Y@W|Ir8vt`+yYM==*Fw6K+f_l)63O{Fz9lce6h8q>h zGJd|w?6qXdXDmny9^IF~dE&-8yCZ_DOixQmQOsI6^=)VmB(%p-BlVzp27y9(xI&>( zinD|{!zKD+)TbhWC6(dzQNBNqgfp*hwlVH{JEddYOLZTsD{h_iQNC?IJI-DR#Wy zn+v8)5*)a6hD(D2LpmS7&4|h{fjA3*(BR8!C-EQ6ksrNvo?`TB-f}aE)lM z97+2x;QTMIgvu{Lp1MmISs`n*p09lyO0)gV3Vkm==z{}Ih>jM%gdLMb**S0DNSKFF zPC1G%;&b)|Et*VkK}IuKK4i6w%a}@TG(E)xv!Tyl+NZqxnsx>Xl$s^$L#riIp9A0e zJ_uhKCoNeTWp^t>H}crw{nXoBl-Zi!myc1;qslya$j{3<4Ft3=lv!Fg!{sIT*+~<_ z)jo1h$jn$cx7frR?(ZnA=w9 z-3K=w(pR*dU%|Q*;C*E3S1H;fr^LRrMxo_4p~yT_O_)?tofXM=#6CfZ;p2Ea2ronyap#bb#%bs zwWK`(L$Iu>-|+emf%0<|f;Ra7*48B&6@tJVl%!P4~?O{nB;kurR z5dT}#MKlEFNKKq0Pf13I2U$lbP)2n$ua6?#yaDt#L#$i200 zPfc5bBkkr=#%wHQo*rBavT?bH0o=a))}QpZoL*y5xIhgw1x2Zfs_OMB@T>#@WOyLDcW4|Jf&r`Oo zW$Je+jhyaKZo_I00R`2>A81n$Oo*UNDfhs1tXLv!a*j_buk$ zMXi|4C*IH=h=|OZ^#<*tc;tWeNShoC{VqCXVt6uKySd@=X~de;CZA3_7_#f_B$YGp zDJj6jHeEQ<8DDxJ)OuyG5S}YQb=z*3GN7t_oz6vHijeK$X6twIyDuAEnZ5NC(5tuE zj?7Z+IqWrEqzfw?2Lu;Ct%@wbKk9upRkVP(tNy`v&8oD6N5W&7OI&X=7IgC=-|dDM zHU*fyC8AR%ua;N4DDQZ)v%0qs{y?Hb)W_cMXi(=Ef<62Q@m6gIvwbxeqa(!ih_eyQ zewG^(T>u%o!S=`d+c%Or1g)hz+3j@gNB3MsKSl`+yCVgVulAFx;Z#qy{9lkgmJG?# zpz#_Ks~aAESs?pvDl~1kA8Xq5>CGmHMfnc9^)>N!Xu(R)E17vtT6ifEy4XMPo|`=M zj*+Ja5yuqyHRpE2FBXl?=&yj zqbZM$h&Mf#H$6};J~chxauAmq=FE@2RACE+?zI4}yEoQpwBKLZ+GNnQ0yC094D*JS zCkDyL&4!1N2+ohiE-?MpV{N|g`Z%5i+vvwqKTHO@ZFD*B2BG=i~gmPGle4U0MHlXCE?Q5A;Y6!7E$9^B) zBY!Rn4}r(Sd2^7`L2LujE$}S1CHFA6kz~rad)ZsMj^JrsZG3e>g323(bKgt z=mQq z@GXi5B&rtY>5X4@xyUH0*QZhCm|!_FE0np7^`}k!{ET-cgz44ChQU0EVN}5by`;TE zo`0ZiUePhjn(~CEmK>u140y!yf=+!Y@ z$Zp3FOwBE)v-e=;RO2+>wI{$F0-A8mkb5{6C|HTOVrPtshm>;q;Oz6ltIq=gRQ|z@ zWDX2Nx|4ce8%-3$z?rA-k?l!DWyFF&>U)zDJ{1VgNn7g@gbE|N^WIRy)#-;23T~<9 z7_IB4uY0MhFA2^m9%5CsY6aT;4yx~Mp#H$v5b0MdV)S<=h{*4J^LhzubJ+#U|90Yo z$+RL9qUUr!ZTLF$oW7gXbF#ZQsprI6&!cfJx~?`6nTXwQp3XbY_j3}u{Xq#KxkmAE z!#{|-=)J3P>Vu!i>iP#)1QH0;X`IBF>5Yv+QPg6twJ@ zKU3YCzMJ6@V<7R z@5)&?GIDQ$k?oz_mV+`17PD7UAP(3IHwnZrmp71g^peULD?^n%V9NbLU9tZiFLZ~K z2C{b=e}Ap&7bveJbzfZ8^pVUC2l}-5v-5_$n ziCLQa56D&)MZXZmHe{>AwH>=dsM9o8kVC zT1;AB-@bhj@7fuEcwfd~ z$vFwRHEL3Niz%cUTBcsSe&vfpTbfoJSQDzUnSYG7FI-I&067VX81rPg*(CWX$Gv>{ z(odO!Dz=A9o_z6R6o-rlDo%MTHHv8H23k%xioyg89LYl$%p2caBNfk~fDJeBq|Khi zA*wlKtvXf3_C0T6&`jz7XcZ@ykMLh4?E&`!WM)5L3+jZbYJx~gdh_zWd+zJK_Nu9o zS^6BO8vY&TT*Av`!R)7;-ZI*jQ!w7;7b&{o-?zyMB8yt*jrq>|;Je(^gJE4Kmd=dI zRb=1f!|UzALD&oTmp4B%|7bB0V zuA>!@h{0U`$Jnl04E=-oor${Od_h@pDdX}$zRYP>uLB-D0+6cjI_(n|2K*s**}$=< zrM1H94YK|AYiz?JBjH?o`3|p{$))ab#Jt)|6@A>!9UTAHw~e1Mu+_>asahzD1Y3;N zX5ddTuXcT-Jl0CZpWHz_WwVV<-E*9{<7*~rMU^%_;f!Y(o}WHzBNc8FGb-nFTgRGp zRW%2i`K*BBnjxhapr4mp*cX?WzUkOH-5}$vtT|?Tc5IyM==9ao_XR>Cd@R{gQ~o}C zq9-S6y^~qFNUvX1`q*di-hYaeC+Z)~O2^xe0#mqsz~0_|m4wj5V8$b%$Bp~!tJQcN z3-IUU08CghY-8I%cft-iJM`=~bkk;m$90-O?ik4n@C#Ks9TuoRhN%iO<`fEn>H8ND zr{duE{EzjZ=Xk7<<|^{McS;((DP)BSwVbeR^Q^h;s+D7qS@!7Ju|w$;^bS|BE%^WY z;>6LgV9AR;$*Y+zelQd3yCqwghjdsH0+UzynoBCr@(IuR;>C;4_KUS|QB+jxfFOF* z?b$7>n#d4e(CZ{XfcI>4C+t|{^;fcIcR1E26WH99?xw;REg*5s5F%8!ppUnk&mKf5 zuD_#^&4reB5!eE?qI{;|N+OMMqI(Mh)x|Cf83 z*cGbG-uxJM`UhghOx!NqAF8;n==pV>z#`s;e;*YP02%Y{CWJApCP;T@C|A3rsc%{3 zHU4@&sNS@o{R@;$4n1R0);Jt<=8A3;`&NfLcFy;b%E-Ri$|E&4#plD$;A9^;Q-kGv zs&qU}>jI25bHxpG02@ne*=XykOcC)N6!pgE;;r>2H#`PZDc2RN+J6A2oBxVwwD%+R zWg0oRMlzI{E)7u7yQ}7Rww6}d3^@p|YSWCgkFD3FgT%n}t@`~+RHYG_h%(NbnyND% zIGh3ZnLaXAve9TOwFE;roiOv-v5Zmx3-^RmkVkXtK(l_Qyc`$>9-sJ~JU!w-86TuS zv_}=hei`j4g7v13L@7s$MdMJw!sDM7xY+xW!CXGd(+%bYHJjw0!3w9G9_K5axdjCz zvRRnc_#0GrZA_QFFBck4a7HxcGi8Z> z6%;HF1rUDDH*;6u75rHbvlkD-(dvPB@p98&(|=|Vp$5UwwppOV`QcR`Y(q^Zh7%Fn zyI!bG#`cAGNNhL9Lhmc-soS@G6=BqGX}f>iHcLcvOwnV(-00B;(U8y+`6V;O-n~!4 z;>=Q8eoIG7cq3(!ylUZ)-Iv&IvxT3;ixSJ^jogTRXYz@))sUqx^fKU#26L0be%E^O ze8htP;qFrCnjI~XQ$n6OTk!KTYW1wGjr?4bvmtze&K!n$=#Mi0j`TK z?o${;~U}u=Nh)UNvQ`l$upaNvW*+|X!wO%o7 zTd~76dFgMc;=5xZSFn7&Pu*avUod20`Ih}DT~0ECNcrj5G+vV#Qqv_g`3mq2mf41^ zyiWQ5ZB{4^+2tWQ9oGmy)8SnM@4V61--bN3(UDjjiXLS(CvnkVk4bahR{soL=%|v* zWKHvlk6>-Di@QA9&*}j41$)MDaXY8@`6)CVnTQais-s5!lFzB#$*eM*Hp{GDZEH{Y zbz0E8+UiHBm@vT5IAK~5b4upS``aaO?u532oSaDwwD}qE%lt(Wv0tdnq!~rsvY+}L zvEn^Kq$w;4Huv?GVN*3`fHR&m2rZGHA|r_;l933dqJ;`WGu&fInkX?@@biyn!N;Ce zZQF*s|L0>5KF5VdYyTEU;MiWn8cba*Y_?D;*}4}dZ?F3+8tk&*;e~HdNzQgC4G8KB z4*$f=9mfU>HE`}W%mNpEdCnpqOO?ujFeDlBfraRx969d#)$FQ=R{+G0;A^D5AT9UoPe`dH?OV~*Z)4 zlTpc^pYzMv_6d#M&xuKKd_qDt7z4Mg--UR{6ABhJbJC0osLaLQjUzC}jBS?U%4lVr zJAZ8fbb2~pa5=mW4tedHeE&k)U@{R{_!bu3THNK>TVHg!Tob|WDye7%oq;k3G*Qv^ zXBU8LNnR%|KGA@+;2-?OXTZ30e zr~Zr}bv`W?in4#slsb7rbVdl~RjZFh>}DYKt1%V_|IF|ZszqpW8iM&>TrNU$u6dZc zH+jnt?*@XeGZr**8cmLo_s>qO=ZE?zngs8Q+dHnU#sZa1BWFBrkl+@1Nu4jAcE_lY zw{SEYL)h=Bbyn|5$+YqAia}OWu42=Q{d*bp(G(WTm`h!Lp!g?tsqz`7#`7}&P1YtJ zb`9SKk3>h}U5p673+q}7hOJr7NCu|Q@Hm!?hMxBO*0RZq(7__88&-O-1ISJ;=41LfoAXz}?3_IzXRLWFW z|6t80hfagO%-4TZj!8SVDaL-_j05qUbxa@w&Z36DWMqcFDjDatVOL;{!Mp^X+MmHy ztn%Jw=gz6ZCXCJg9YV3+uJAf^|K4I=Q!bJPo<73@^`?C004aGdHM~9)0K0OxFC{Nj zDejb@EhF(=Jt?L<_~{u>^{)aekFvH|lo7_S zE-7t!*O=TP-MAr9E|}>PU^9~-xSCkB_EC}E=9h?s&7QswRSZT{X76D%MDrnk=hX7( zlMBq@8`d2b^gf}Xg?POUmM5C@>FP#*D)k(tpU~3s;j?vU&WzZGzMQ6+5b3s5DXv=B z1Kn)Q>(pu-M{4`(uZkiC0M&k`Bo=`l*GJxEw#34e&KGL^vqx=YDU>VcEAOZQaMoq{ zi5&VHQ#kP&O+3VevlHr?NjZ#Z&Q4Lq=YBL5u>Q)dPk`TPlcA~O#RSf`Pxf6i;Jt-u zGg2tu6`Y|&RUoj8iY8#gzDrbYS+g$k=Vs0hq!IdHSm-6qTP5y`w#`45VOEXl)4BZM zqk=bHZyX+#Z{$Z9iuab+EKv57$3l;c!^fwHd!tb3IUaxR;In+Rxj$l!NbADf8R5kI ztGK+pHLY%16oUU>XAS54fstUv=g_U2w805q5ZHcv{F^~o<7kU2EGC(c@x`f{lKB<~ zieS4mm4LS<+P)kwV`G_#61B-ThhhK&|4m)TESKY8Camc32-vvF_NpPp_>3&I@esnQ zm!Hm7$1tzV%wC16yW8u|YK;|NiatMq=NCk1GZzoFYg@nltyJCnth#!F$YJh8 zok0(_#0VNKPpo@AsUGVPJ6%{?8(VJ~?Vdwuav6srvptBn0?_BzAf!U74i#0en$L7&?CZv~cL;U6 zF^*2d!-+Z1>)0!pE?0@rS~D5@Ij=1)E1QQEq*X6B6VWs!I}0E@5yzrVYQERX07{)J z-h4Eh?u7y7KU4s-{#RVmabxR&V95YU`qpX}73^KX5KUh_O5r*Bnj_G7hX>p!WVe)Sg%wyt}!~$NJp|{%j3*N)W-; zyD?h!sxA8}_6C^VK^vBk3O@cwI4S%dV|`Bu+0(o9OdHfryOET*p&UuG9i%NzxjDUk znEEQN{2?e0)AiDH@Y^#JTjeNayjAFZIMs5KJ}DLs+| z=`KH&^*CiJe=A9A>RWra{{`?^QI7wjT4D%ySbc5FI%Z|(z-NsdGc4d*(GTGC z|FNl1`1EV_{MOeLQdt{lnFW+DyZ2xSx%Ds4=HE-|%NAENmW1WSPRPEap`-bJ zJedBoRcD0)v{BWV(@3$qG8Eme&R*Bl_bge#ejU^q!Ex2%w3iv<+G`Xh2G0!-p04h_ z4q^J?%^i;kwEdAyS$dctWIhaAk42<5xL|*NoZV&+{p>LGs%*~kKP_D0-fdCbvbV!S zsnPAwmaVeZQ)BHL3=v(ne$8-T&uOM;^%*|}OXvq~xo{-fK?k4xPND5O;lqD9LxCB= z%Ie2rUMkV^@q?EZvR=6jtHHHtxY~66OWVe4(sti|zqh=yQgbh6Hj%ey+X}kWUwJCf z+}|kA&XII~g>}Ph$m<#_u;2&hH!)F%f|Yjr;a8_VE1KI?*XZTG?tQs3WVaf7q0Cx7 zv%V7Dtk8t6|EZF#e8i2{hPTUJobgk zU2!9|4v}#elhNOSD(_C;S+LdjPyrL?d}?j)JT0QQRzf3{Gt`IN4}QUKFkd*&9Sug? zMkbVJc-EhspecfdbC+dZAze~~-mX$PMtZ!|{(w4*xigDY`DHAC%?FS`bPc5%wg2)(Qjth?JVcf3w& zgjlnUg~0FNY~J0uK-&mY{8!(zad%S>3QRPryNki2SY4p7?9uN5iH1L047QVAY}Nvq zrD{^nZ5m&@RPy>m$z~$Yc$$x{U?VJWM$f*Ze|2iE%>!|zpr2_aqxesRLV zv0cp$yIP!!y5in-*18EI_8Fg#_95`Zeo{^h3k&Z3z)iw@X*9F?ACjQuf+BJSt6$3A=@G06t$>TjF z4jc-|l;ROIFuo79_$mVmwm*!Qge6QS@*ydT)hX-vUh0Lg`{b$1xmH05#4lS7~;GD$_j8#GM}tChoeD|g$tx=wUw6Xt)_V4Hk(~nONn!*3zcKP(6ZcKjN__NL;>)Zc1CpMCCUn7U7 z9^U+|t{AVneW*gJ;djr9&ou|}BN4aGV5D}wUty30g1zkw?9Y>lpC5FX)uo(`4}Fyw zVqPH-jO{+Sqxz=mlb+sGey@-@Tpe?Ee_nvhvg9C}rPVH31)HU3+mE1IMoQ?1r4}noz*O;rv$H$+pomQ=%e{WMP#BwOu?|*S3`0!U<{Xc#G%;6L; zt{Sspg=aMJ8J0u@`+4IpLwcZQ5&;D$X_*7ngKy@_SdR@>#MmKp9fp0C7`;CiA;T!( zYgSsmC5ZvCq=+dx2u*n#Ckh7CRa#m$Z$Mpft>pMbPn@w$L;QI&wqM4{z2Q31(geU4 zXoA(PIjp|ol?^(qSN9<%Eu%kDO9H@4`1T!QjIw?BpfBqpkH2(h?{WY_0l5ALvxAol zV)I`m!|Jw&@HhVh;`iw!vz@d!*xBv6kmmdg$OA&GyiD_pkl&bRcz@1ClN3y-=Lg%K z-G@|&NKrv)b8!35kxUtpK2+WuIGpR_BVX~eFc*SES<-@JtEl;&#;^(cqXdIO9$1xz z>af{U2>xeR?gU-g1s>=oHgD%=h;WwHknj-h4~=&q$1)R)2Tm|=Xw;K76##AIMAzsna!0Jv zA^>0>R7#qI>>)vsIZScANBw;mJJz-at}+JtA6b35ibg0B{`yB^hRSPf4cFMzG9PKa zp2&~8I|ipDhIG(zyF_yJg{B;0o#i!Vs_a2eQ!M{OKxKPW)+(PL^)VCg?id2~+#=YW0JJlVDaPl9M5=C@xYkl)_76q*O?A}RZL z!+z2!Z|G9y%3@TpMN7(=LG}7^zVf)ZbnZ)L6!=ES_=FjDij;?4byGFyj9VfgrI88K zes5(k_#Q&iBXtm7YVj#4DL(Q-h`-(jW>w6Rbo+`^yyj_VD7c%Gy+FdWgirk~FtNS2 ziGlz-8-C)Y{;H~lQ1Z3!pY5o&fM;G!9iq`H1m^zjh9(mQ){QQhrXdHo&5^ zNDdg>ng5-WLzDo#G3LjA0wrJE8E$t^;|S2ylc&YLEB5v9Tst+#yl=Df*DXMe66YT54{cJt=qKJTF`ZWyg-{?|BoyRzNqA%A++`PgCWj1LK?3ZETrng?kF0HJB& z0(1YBYLQb#Pz5+we}S>iYpq-AxQ<(Gh>%N&^gg!^jGTGEBQy;W^|QI-{MV=VAjRhW zZJP@%?}@EjO`CqSfj6lB37?4O{3QGf%Mv8L*0p=s$Fi{_ehjt z(dq=xXr*17-|{2-?cM@eCdN7NB8F`$KismexGbFgp4ARYapz=g?nyP8SOSX#QhBh} zrJSyYp)&Yx^3NH){KaDDAEVeHUJ|lJ@~I3k6R%0J^?-_`f%Gw7P0i+mrTJkDqagTt zr|Q3gDzVh0HtMw>q}DGl;h&i=?K6{u9DZ&s@Sh`oEV>|};VpeYUZ!K2R4!(o?Mwm( zLL-;bpAU-4V{f#tdZ4hdJlacaP8=4nCD3pHJ+uoJ=!I{QJ}{dMXCt#*miNT8Pzt~S zd%faEN&Q%T(8_pvM1M?_E|!wcdW+raJ(z(IKWoa51ZA{lG(_rcf3(ALrb{t46=mD< z&;;9M!Rzhld&h}j4@sphUJ8wFXo1X`ot_fT##;(ja$EjDb} zh+9{AZQEjdHIf$)D43M<5i$Yyr;l|McXs3^G?1%9_U|gk0x-Nfz&Ixe{yYEKTnyM} zHQ1#Px&^FX-JL4SoY4-dWwe*LX=?1p*1jC6o^r{t8@!^&qP+;B+uA5kbrXa zSQ2-%UA1Y1D0B4;{x<(jgzdm1%ZeDn@f^EK5TOPNnVFRZp&3&19eiC|6}R~_o!e8v z{Ydc7-8s}<{Jr$Sv_o2Sh_+*OQ6#=nv!Y*S-a*@geLpqExr-N} zk$DS`4(vCZp1=CKrMg#+#*ck#zpI*vN>eXs8 z08rl6a{Fnt796gf;tX?|A(DZ3AJ0F};1T?4VP$c<QcMw*yyf&k7k;4&2|DKC zy4B8qMu8_o)>NAVK@&v9^8^*oaN87qiC1t&kleU6uE5KS*TL3a>PH(d302r~-eZ%1 zC-B&xo}ibM3ckEIBE0N{v6y+qslLxWj_a>qmoAt#<2fF0HD}S(kbw5!zX19-dme<+ zs~cfjR&1dhfV&f)Ys{Oj>~x1^s;88c?7EaSCtPOX_}`rj`9hEX=kgqh`Am%n@O=CW z9aao);jkjaAIOt_0lrDk@-gq1&-Rwmy)f6t1lsK$q)hvQ{WS}=djgA#MKhH+neZdP zre|pxmZ6K`*VM_a`oybt<fmAn@q#P||x_jTyK$|K>` zZaCGNr_hgJj9~Wx8!Pifl?H$1FwE`u`!={6G!YUH5c#(H5iQdH0l+t?d5%7DeKNMf{J8 zZ1>-iuNqvy1I<-5_K}u0jTsVVJ==Y_s-d&M9hK6HA@{w@ble# zH@|C^;N5_KW_K640(c>O%kRS>N`jz|X=dtYtvYB^@akW0z<2#F8LCs2gP21Uys?`S z+n~gpwFmPE55-0|tOTx9{c4kAaU>7@MaQew5o5`Z4w=D`*o`D*(`49BB1XW$4wn-+ z1J3|>5w~qB)p5r2ml<3~GMb7IKLOcX25bniu55ZD-N6=YwS)*`@l-2N-GeK$kw=gX%%?dq?f)luTNI=!mmQub>| z;1~ZCm7mb?0D6pMJ;G>W?PQH3$myT81E>;jafd9}to-SY4R%e*&^JM(C;%K^{ags~ zOp4hKx8&PXgmRuBxt$eK4b3}W$5z?OvB)gfoQMzWy(N_1 z&!)xfzqg#dTRXII^{J_xadl@~NoZf3e8y&`%XBc@6h1de6(&z}zD(v{g3DT034IDt z6)||3w+EKp`e$oV_NV_{+}`a)hAEP10|lTJL$Q}I1sypOum3LjE#5+Bv)U`;S20}< z(>Vhb&(f5T@ApecNW?l#jFs4Bxr%zjo8$@k^2Z{MAAE10-o;rY@$o&Y@VG8_HWK*M zpXurXP2XaDY@YPFLyC~;;sc`>F?&`6QPmwZB;;l)!E4rRVtq?ug%VUFt2cnQXjAsD z`AOQ-#ZL3jH2Q-&C96WE*i&WC@$5X1M-FA~U-xFaZesIvDYtQ$B^ynUYMB6wU_)VL z{1AAqwjtQ)5jgbs)YKK0lv`S1?llUvQpW>ZWyqtW%6Ip|KskV4;wJ1f+(R!&AUuzU zrav|1W2aXL-VrGC^kVEAAG+#y=iM!iJAWAC3oP79%uMaJB&GiJUxYK?^t!|08aekh zGPX>jA_pL!83)CL&%F(33PB|o-rrgrh|hm$3xc0t$tfm()}a?O-i75xQnYGipm`@K zPsJ%)!pG@9Z=LRM3 zlMZH`PVbhC;ssD#<4`WA@Om5*ETn+JwU2SC8h{51s>wf&#Rek`@d4fOs&nvXslL?N zwr&1gxKivAt6c;^1RF$yHE8szQsC&8q*VeW+E1bCa1hI z=9CJd_7jVuO;5@-mq=P%E+?TSMSl4@q&yQP|vGqB&d&yeU_CwjC z7mrdmus8gwJrL{tR$G~Us$3+;kuxB3Y7F^^_U;3<=%1)0w1P7DW2KhL*`(XNWwWF!#fxkLceAi{37u9M|n-IU;-GlCJCU+FZIVqzgzz+_-N>NrYQpjWb~Uj z?K>MJ{l)^to;5t`195GUyRsIVkwCl>6a7ja_Vn|W%kk{fZ&H^#-qjai#K^=hRosx4 ztBAWMw5$(EoF9XJJLA-!?bl=Swz(F-X>XOp*gBzXFZ8|7z8(6MQ&=~g zfTndQ()4Z8wYkJgTy1p+Osc>&R9Dd9g5rZh6=*;$etXMPQi*~lvE+fi}-@0t?l&T5GXJbMG$~HV{fGO z*}2kas#n$6wgvY6n#-NI$K&(rF?lmh;y4G#Z(oGEWB+l&pQ+b>+R!jBVAE=lZSxYK z+3vVThYh7G67i0Zh$nz-J6}1C&Q3*)SIkWApkQ2g_0c;3?nC8*_Yd*KBM ziCpnz*&STaj_pA^EC##zJ9|EAg-3+Ilqa+%r>DjenQ#k|Iu#j#vbd`s3J47uf5hf$6I~#@??3VcL}G+L}DBf zpB0q?aC>{**OR?j+dR9wk3%PkSM#S_>uTaB{td^svX!}WzpWdJA~FBKD_D$A+)(K^ zZvWTco&t*=b^5{&Sb~EBW|euNLRXtIvfiOrgyB1h?`ml^%>!rVypmq^*3o7kvb6t& z<6AWHkEEwrGtkBmQG~be#Z4i@kf)}*FdBr zS999F&%C`azZv2+4AExBSQ?ThMEi`TAqrV5MWJZZzAwg1Sz6F8ZIY0rq=lX|3G8iEdFKiK_n>VkfW`BH$JG;LinnnTi01;B)7*t)fVB%i~_Fw8fzxofzVN)H6-Fz6pZbWZ|l zk&XAo*ax`7dfcAlr~T%;dQSbYC#iF&k}QZ@bFU84j!52K5}~H1q(9R0$>5!E7DnAM zrwQf*Jx^f}0MYkI{m)2vrRy1-pT#B%rm{=)SBh=DkmtK%4ZI!*;Su{ev&Wo;0bh0j zCojX`)r^PnSRq4hvPmBT=YjgHw#hPfALETL_U+9r3=(b%5jsdeJ3gBaBjHAJfkBf` zeyLNGMgY2?XU)71O8SoUo9T@8Gk(HHw zoTuR`xgBBf_@$*~+o|67owsm`;;!B9{r|*%`-?BWH*=(%xLT%#ZH`-A;Ue*CJ9PkG z?K#M-BDK;aIOiNUm!GdTs8z06C2QNEflOFv_B932QY}1qjfF->t{PuB*|2|Z!@MM0 zZZQgErB@BB8tfsnCQ?d&o;9`T&iVO;o81-bvgh-?O*mVG|8YFe`?d+kgXnYFcaC&5 zPMsTQdEeXTm70VGs1s=nj)h;qulcjC02}phcS-okCH)ujoyf`RdJo)oO{cz4P+aD@ z{b~zuM%LxdqPVd%erQQ0Y(PYo-|?gBsIz{YEJy7re0G*ZnB0klb@RF_^S|J|oy|bjLDJboQ z1160zvw3x<_^Tzy7FWFq7;%=Wjr?B1nFviwveZRp8SRT`W80bSJz7WEm)a46A;nS_ zh621XvxDQSCd_};Kb32Ox7G&@WsODRo7x?gQFTAWC|Fk#226$-bt~TYq9v(*u3=?$ zFXb|d&RZ3A88_uD0{Rs)ow--chif@9z&O6~&k{T><3M^J|4)jWj1XBfyb0ieZ*(vM zg$P+8XaJ1)ovRvnuMfYz`P~Tw_Bhe(?`uLJzxodW6F)mR^ZA*~r`fPgH^zc1ib6t+ zGvV5a5AVtxe9XI>#c#g42_9@HXMwc!w<=^!hWg(P4>$s@`|RnyeF{-Mz?2xJPa*U! z?wNEaBkEsTEH!e56ha}}6(o9h>zn*8UBo`yWL%xK-oiHBdL?62jSEy|k-fH@QY9e8 zKG_>3Aotb7vwL1f?o~NYbUXtOuYIrg&i`QhBJuPNm_-~GvOY^HAbz$A$Q57g9Z|9< z=ENnk&cd;ia6(zBx6n9x2AN>G(@KI4KsUrZjawbB8E2q0$8@Lg1le->56EN)JEYx; zrJE{8FI;&?pW|nut_tzoknO|JwkXpKu-UKsuS(+1sqX}zQS7<{@#mif{ICr>EZ*=bs6R-SO$nGuD$EywHNC!4x4Bw_h$S(|to^`?%(WG@BjzDWDOVkF0!r0UG~YFmE95z?_F*qn*mQKmHh| zGu}HjD%&OB8x=$#>UcvM}hU= z&o-#_t+@xBdrKJzGYza;?1jUUFD*?}OpEqKK0BV9P!E>7vlpYf<~&AwQ(q7BTO_f$ z04WV$iFo%#l?5!H{dH%})O6d3{&KNvIZzCj`b=CPPm0Pk3*c|jeufT{n!mxaM-u`XMp7 zVQsefz#`^OePPW;KleAu0ODLSWtzl)OrBR98N?Y5ne9% z&R4=}c-e{5CeUx#=e8~bn@iv^RYtscA!4D?QWz8ob%S-?g?6((J3EtUK$8^u_$e~6 zdj*8CGw^DiJET`WYmZIzc5^C*vFg?YMzqnR1)B2wd0i*qk!I%Pw?mk4A8ZPs0?z)- z9}o>4)Hj+{hh_JCS`<^WVb;payin@(kbqC&rK4^EPAmLOm11nm=K{G>)T`IHrn^E` zsNDg0Xk7=XvA@YNBy`@f!FIWJPJStgX*#b=H>KbKf4F{IAqDI;ea4l@FSEF;g#o9h zBBoZEfxZ_r*#oVk@&(J&Z~2)LUp#y1X>^|{z!s=~X<%SNZK!MDxBRI5;i=(gT{-)| z|9-WM$^VUS{@0&xD`bA(ov~@PZ`{UTHwOocY_8uS=2CBdVL-Gkj%VHvE2?N${`&W_ zZ%=7042%-U*&OwN`}fr{-$xxY``G<@rzNbX|9Ce39 zEU4selJysUI5%(x+PsBNnb8VU053c{YR$SkDOjIv)pdOk0xwC$Mh&;Kxpj6j^N-dE zUw?~rH&!^;NcG*;Verg#+Q+wS-MUPz+GbeGvuArP|FLDg4wd?@kO=M5 zwZB881D>T$jvet$PEP(zA@3K(|FCiYNc9oYVFV_#-#dA>+%oz5@M&8Mm0@a$_VICz zp(;q&dYvY7q}nNg`ucTj)XSbKGQu1=VfVwZs_Cf`-r(5k?efWOPq?7AOiqH%7HQ=@ z;ddk8S8-d`v*>ItQv^^({1Qaa(n?dM_!2u~bhu-r2MW#0@vj|S8gm{%&!ElF_17lt zWP^6kii`)zLs*liuA8UfiYeu#>*bwRq>f~S4B887Li<*D>sI%q;^Jbrq~ENtT%OIH z)+uVha_<`0H3KT%Ye-L7sEpn%Jy*S@KB-?l=e;aI90)2XX2LP{2Wh5yhSoM4Fl`OS zN1IXVNM1N_&#=)$7^D3cU9>>P*MpU`w7!%Stl=KH&I?x|3r9f~wn3*>ximwu%BuW) z`Iv@Tm2lHL-xVK)HKKIip1*u~i$yTz;;g8MVdBKGh#R3@i7nK-P!p^g!Jmn7f~EQqF`^5%s58iE`2HHU>Oy9etLtq^pRCbHIg8;EVsTCa2$7`vgm^n1%oE;q{ee2QzL_8K32XO3Prj7p)3iYsPR*Dmtq35CX?gVWr5rbqBnm`Zzwz^y~@S zN?EkqYiI8IK1KQK+ZIbgeo=3|-?HSX$!;=MnK*mi`daRM`I(EOl+_6@4jT}cx>oiU zVI*Q|HJWQwKnvVOs;0+mV*Ld*#n4m>o5sDoOy#G@y?|;4P#6YOQM@ld!7kt#R z-VCAqgdaoa7^kiFa)d>3%3|9s?GD0~`uq`H?O;#lV0mb92JpmVm-_!&Dt`@%u)6y&(t6}$CDsA1j=f1gGxID&y+7?x({L2ihXgtq0>ZX@$Sd9+VRqc)Mzo5MxUhi759) z$}GOVS}U|Yg zL64%x)9utfmI|bHv|`|$h_L=phk4U`S@dxIc*|?1S_7g6m46F&J+VEOJE@EBukLlVU=oVL8ElU%7sNTWTW9=&J%_Gc)(nYsIhmJOQHH!aYH z6g~u;)FG@Xe_*O^1&HHM#c3@P%)3paX{}hSW~JMgIpSyL@~wFHy`(fxrj?TY@lx>B zo`|LhAFndRSln%~)DG3jJpH}G@AS{kn?K)86l3xvrZ^u>Ms_T7a$MG?vjP)Jeo_VYkGyoS#U*;#x52OuH183gMG-L|5?oIXub zp3fXFVCWrmp4>2cOcDu+6}7l3$WmyBca0GSLwv&>9Y&;h$A8yj1qVocUQE!jxj>ck zzMx$6HW0(K%RPxTyyJc*7_A+OQHC$T@o=SMN)fm@F2GfaEM-=4z>txfsHjBRMxilGM}jf*gRk>F43fdM2A-rvwAQE&~fzjHN(0 zgfG_EXyi|f9_E!41EwN&;wx0SuzWiC3nV9lOBcobS!6aQ!mg;^!EUlyhw%)%>-k9* z!g@LGI^0Tg(IyW)^p%32LNmnW^d9kAoI5a8FonrNP7GM`aYGf_*dVtZ`6LX7DeTU9 zme$k+BpoKiVJ+!@Rtt-M@+JCzTr2^(|II>Hr+CI&705zyPX}QRbFu`24K*SletW(1 zUZOod{-Uew$?=?mL6rC*b$_!{dg`P=`~~gR%5@BKZ@fZ<0D7 z2FI(`cLyAqIX}Cn{ zNF;Int^2YY{K$Caw{fD3`oBp9fj^*`jBCP(aFTy&N2SBQ@s6b;LP=YGsjjA?K!-=~ z70~EeqCigc#!h{=sX6+GnSe$8i zPE-UwIdJdS1(hoDbMfmbx@F6u{a8f}MezeKCg!838UIq+!Ti^r`Bej$W(cKD4(c!t zqixGZyYcqhrBvw~Pt}d=wXdCy^;m0M16*x-$)L*x6}UPsZ)EiXHE-O2w5v=2jYdzasod0a?Xgc@zkdDI z^3Y>WPR@)`XgA~q1w9p4waXP0Ru2nD3CIHp4Ty1qTakW!X+7!!Y|%atIaH*7*R;wY z>xdYY4Tn6L%3oLyJ<6&VYk%Bm3Vu|jDN~H{F^{e;Z(Fs@1=l*mzuYtUSZ0-k#S==%ul+o=z zv+VcP$RIlT&#R%MlPeUr8Vrm|hvRdN)*lSzLCW=@b_uM0Za`Qz#s)+_e0pfbtc;~w zYENrroj7sg@xj&3SFT)XaMq0}U$@tJl?RHzIUhsHzrGNCa4Dwp>Y=|#h8)zQMlsq6 zZvaVDA$T6_D$Y}nNA_`jxjizMt`hyb@)sJlW9>!?*DnHT?l8(+iIQlG6Z5qLdH5(< zReEB-W+K*d`* zc{tpv!Q^f%&ZeDp%y5FrMf<$BjX{olH?Sj<_@N2aep2$cywoNoLc-?=IQzrTs{H>9 zf+fpjnCcG-Q%$0f&ympB%QH0%HuX%n&yIXYE-qY@=@DUZkhu58ho-;A)(w$s@Vpt1nOYF?=U{*nDxEE>ls!{`m7c2yxGku!1X<5X^~!P1Bd05{*uv z-aUcaSSB=o1WjdfYh=Sp+9a(-H`cjyvSB@XpUs5bx>@DqC8Gk#VW-ok)-^iMUgAtnn3dEzxI@nvD66j!P$+<*V({i13=(ghSKOmC1({5<1Bv-($`@{dh{#HsMaA0I=2#6}~yc#Wq*m$c@;nwTN>q34jObtNl)`P4dWfDaAqFXZi#i{p0sdM+lC>I4qYDO1Syw?0;6l4= zm1X-53+ys(Gf*dk1_?WgYQZ{Nv;XuEYQNjMtu?E9!}S~ezfbftK9bbYuI75b?pp&| z9~0=H(PTC%)5-haudgq_Tg3b?GV+7)CZ^xp85u(qx~t*nqaih#t-~;#3wG4mw`b&zHk%{6|)CJBG5 zSpQkVFP@W~HIUIceOkC3@|F9nI;k#Dc`r3$Vd(+|iOO-&aI%o?q}r5+)&2n<8{>CL zu$g~y+=zVrteYg%2lq-kj0wj&e(TG9i(08BnPrJpi?|*?lN#?Z9|)C(hgv1VyBjPd zHdMt)Y^3I@ZF$TBlveRBc9GbI?cu6;2ShgafbFLRSRuZrhi!7A8g!AY++r=Eo?HPG zHe6g>e|&BSBj$Pv&AD42 zsa=PLLBFOHx?vr&+A9WqfBxq%ugJRLWi*GKY#H#$YfkRE+nB#mD8Tz;;p@6@xiBN4 z;b`{Mn3_SeS*mZiv}IyvT?5(Pjx#_K@THP~iQAXL5m$*jzK@8v*^TC+6(Shid`fpw zMxGmLe3XDJgoe^Irln<~{Vm%bpG>v2>62GbP(Vw_ZUr&>9;A&CnwyAb=0ieu*wF%M zovSsyF3^Xd#XS90>M|7Lnc44^XJ4^`%`bW*BUwfT*TT(eBq2NTkYRUKd~f|?yB*4@aG&-TG1EIxX3?y}UGcuQ_Dmnq)?yvA z>U3i?+Nlq#aK?|0>2lahKp9X)Fwm3mPrjHe3_kBD%l|0)H>SZVq81AbDHE>l&_L_W2wk7|k7wky(J3qliV zz^3PFyk>eCtPK=bY<5CNcgyNK+V0)B2Oc*R&d>bmw4N%#?)Hr#IEGmFMXk&lAyCH^ z9`-7u*y5nj$NT6nqx7_7=U^7R>K}XyqR~xyugg>Jy=2oXvGpOMVC*&s8*++P9lG=a zPGBf`j$K;%hyIx}t0@$59mZuSf&N!1khULqPzF+(M3V8v7*6HxNZHTtn#>r_J2=<#0^Lhkt~+^8*&}!@+OE8yF#xeg;25EfJX%A!I?k#mjo1r`<98gXJo^ zxBAHPJI7<3Uz=b8OlT}+IAZ~QBol!U2IybSUnKb3PlVs-<}*23!nw!HEU6Kyt$IkO z#;rw1^s4Q&;r?O(mZfJ2M~=vrEw_>)(-w*EMoow`5Qa3s{Gn*89oYimp@+(;10f@$ zkmqQ3g)^``yiiDIM-t)1&tfVpjgY@RW}po zBne>ElhR;2Xo&iePOpV;+b2M>5(F!$zS;CTHABA9DadIRkw$Xoy$E9r- z;2`$FWyeqCD)6%bWHW?dA{cku>1wCA=u}PWc)TCW6yAGW$vNK#vomm>8CP~5E@~{ zoh3I~ePDVZ!Ysw`KJDd?u~AKOp0{-+YP^Q5j*->|$m&RsmzOn$2thlHwpx${570Ut zYuBl5Fu1cBci`}{Uec79O7^B#$ys_58%=<=18el|{QH8*~|^e$nb2s%a3F>F~j*Eeot0)6(3h?L2}r`$2( zErI6cWDr9O2mdYe&EE^;bi1%r9htj1(Y@l|ImHAEB0E)O7kqW^m}%)V7r61c0~Ohs z#_kSngC{SKwe~h7599h38)Rif3t7L`_&}raQYyM5zTba2uG;ufER&=kgMj$%7d}?| zd8Wz>GOrMDd>(CLA8YK>+6FoZ0mAz7hlDH-laaB7;{fwb+$zdZoA~LOMEldH`t*DP zLOza#qJ-OA6>vdDuxJfWX;P;y*(|VZH}6E}j1NyJXIvzcA1RJ?Y@l$4y z#hYB58!+p8KgPCG^T)+(D07gpW_apgS7O#vu*{!b+D2gTBD4n~#@4kno>#TyiS@9m zncI4W%`d-o*91O{?V{UBiWC}XH|xufzwo6I!Bxoo44_9t|YDJ~ixPjyX`p7>NHLaRMR$xX3d0`tv$ zUC8_|iX*e|^jWsc9&Cv-(0C`^=zbF5Dx&^apI25SVYS(=U&X_7fZjs(slQx01S-w; z2@IYud}|hMmvbuDGXorNls?A|`mxV}{RB#%nj1fCRL2=uP(-7`NtmCp z((4Q#v7x~N5^-9GH(8b@QrnFe-~z1kqC15;0RPYOdOK&pk5?}YO0jsmSamo7{vv3G zZ_*iEJDNZlpxQut%@C(bttqvcYeTDzKS?k3k~*^Nz0tt?R&nTqjYP7LL9`4tXUkog}H^^b?=B%B?anyiVeKkG=IfjTG#L{H4K zLZa^4n$H4@hAabtf6jX{Sr&az(P4)E$$Dw^Qm^DqWD;sPjR!MTMMwJKWKkE_w=Lp2 zzLL{+`QPYU$dOQh%??rqyl7%tq|oe-Ly%g73`Fxy=kxqGc%A$KdJxMFYFUxqnCsLv zHJf^FNJ^|bctdgdrfavCuEXN$xj9JXV_ZTHnqJ3m5x61(lai zt4csV=rcBKF+e(EY9=}ozO>*^wnC`o=(BQWUDUFH8(M?}6yk=WLZt%-ky0KIwQ~dc zNM+}!tF_WOqzSNYu8*xJ%{BKJluxb7jGD@VcsC1B@s|=hFNZm0t5yuEq%S)j8 z#uG@1sD3t{dU4@km3h5$$aY9aXf(XN4enJRH4*-BX`B8)-&3|%Fgx?dxM$X$Q8CC| z8Hox2WcQ*tXj~UQ&qSKf%sfcDcLc{eanItyR!0Xzh3k?~*!2P~K`yW4 ze`x+M;10!@WR6|!HMN;jfensK?qk`J?R@OtHT#Y#%+2Vw0eKr3O zx*)bf9yAAbWyx2bQ3KtRoB86#+H@G-CU&iQ0m^;CG|}E(f@o>5ngLIoH9Kv7mTH_A zaS|xxp1FEb9}0jlr97O$gPb5kovGY3!Wanw;t+@8{3w?eb}5Ik>`yHlkxH;n4CKOSY+ zm=iZxqAaH?A0Sno>bPa0#8^u(H5#?Fw8V<-!hfC%>3k~tIBc>tdHo-QMwoR2ny*q+2iEfru5K-SJAWl5@XdHiFg2E&phi0~cJPz+NSO(W421N}z-kbvyu3bEuvpI3M#$`|gMC>N& zxg;+hK?t}6R>aYqai7$W@J}4?j8y|B9Kb>dVcG1(iL%c`qLUpxH(lQYXp0D`t%p-g zWas2Krp`2(<6q-oriLKGWG${4$%JuS+F*0X<+8H|IX|4(ZSu~$N)e#30XGxKlnFB* zdp%!c@ap1q(|};03RZ2|bX|zHKJZBTl4(3LdYImu13unI5ZhS$=z&J=|7eWa<5e*-P)usGvgenMLGHu6 z*tf1Ql~GCXAf;WOabX@NF8ov7!vfpw%ai;4;x%{gPJaNpmSRMT*xrNpyn%-3EMbcR zKTJn~)=t5YT`UBO9aN2O+fhwZ{eEw-Lj9p8rHkLfQ-hQY$ zW6dIEJd<>bxc)bYl3q)9w@~r#P)^t*ou_@ zDuy1+E#Y^WW=0U931$#n{Zm~eng`nv>;zVCLBV65))`B>AuW`B%Y{@`F7*dqU38-} zrV=i4w+g7y00o8R*RnT)>#pxU;PJ4t)+cx`Yl)(6N`0Z`#(~EtT1z+_* z4Y!_98b}w1EO_l&_;A@8d9|pYW1f7(RKQ;FiBsjQNwk)J!PApV!n#t#Ey!f7fitI> zNeuN518s7Nk!HTrLD(@u%M}BP`N@nw-5H>1 z?I9fA3D+qu>CuMum|4fcLJ9BG&117$Id*vifA^R)I5Y#7^T zF&~fjlA*Yw+-7UY%FhkW%?tc>)FCXa^hD4le3PZ_p+MWChPIae$EXy%p1`F zRq|ezNkM>*zK0&^2&<%oOc=)J2Az=XCeAb%6fIe@6!Kc+Qe+%!IwA?C9_k2S4VRRI_*1nAGb6^O+!Es~sBIceqlERJBWA&t3{VgKm?Gzi{c zbIG>1QK*^Rf_gLhPU;pY_}wM9;8>>aR^}f+AwbYwkJuW2GA$@gOy$jCE`~^Or;fgN zRR>z(8?J_$?;U)(r$<#w<|+{vDwtUhCmK|)NcA>JOHUql@mX=&9Yt~!*+VrYKF%SU~D$B4B;(&NbW+~a<^ zRE#2xNnlhm#P|X_*$)k+!|jKNm;q8Eu;g%j+4)Q-9h*tlIjRFLTmQKJ{E^p$C9sP` zE@!YCkR5XB8ja^RA04A0EtpB)UVEIt(_42T zJL~TZw3(((P@~fv7R*rkJmbkRVY8-pWhhcGGXU_M@fsy-jMS9E9D#A6kZ(zZt3IFc zA%JzdyAD52FCVVqKTQ_fWWo}g?rPsZRhJq{wsvZ4$Dzfhlr=Lxm3L@#C8|XBCCw~CYy~wBUuxI)xhc5JkBDD4?0)SUmng(^ zBG2t3)JqZ`E~0N{b_*DiQygmY@O9OC{%qjwV^Kx5dv_?AZ-KnHpnn3f`+O+xyS!Lx z%0hI5;Qcw)l^nbxx)QXvZr9C8_!NzU?=Y<-)nW&HcCT4=#fMTfiV4+k9D=~GWiS|D zI>!20#ex~DgL+Tarm0@n`ZlBd?1WE0z|ru4RL?8ySkts@kO`qsz$)I~5M}%ENnFPR zx4PR{WtUw0(@&@n>ITRqs5UDgm!Od~Dcsv0ewuruUdm33(B_t4!c9(s7aY4C^()WV z5&g+^d`d|>lI>yo}cc@<>#V3fBF zD|!{$5va@ju)h=ji8#s+wwJE|93K_tbYP9R&3qPxcl-fM#!WQ15wC;F%LA#C+8tPg z!x!T<`N%IfA_r0S{ zEe8OUu*7;7@%yQ%snM#3ml?eIc0Kmgm2#P^6658}y(xU`!p8|liSC@Yyp`RUNLeJzOrCu zPn461BvW9eB%L7OF`VKl4QmZx@!Qsm^3VSz(fd2hR&eD|EaPE#3BF`7J|BjeQ}GIt z9FR+kiK6^*lxolxc@EwrpI)+8%*=-cH<{)D^$ATaPm8XZ=DSCgB@rE5o*aA|uxV zvPK*{*Yl7xz#gpxL8yc@5>T(lyI|`iE)7rB&Ns-0>WMF;eW|2Dw;A5iU!kq>xpYwa zleMKXv>=Sg>6VK1LHzWSCk9NtwcAS1wJVhA#(LYdhP>aVzB`6y{ zrrWYndPK~|>2dKxq|44TgKZxT(l|xkEAXE+J|oN}MKv{Q`!Q&Nm|H)JCzg_y)^Lbz zxa@D+^P1!b4XxsbfRx5YvNR$f)j|dgL)a51X3f&`XTXSx%G1#5BhGE$5mEIcf&s7Q z&|t>MV?|J|>^(_@WUI7j(E*Ojh2-fit9Jfs3e_Ey3`|zuD_xjLTp8m2)c`V8pO6ffnpKfC%nyg(OI|wKx5$F2 zZ^uO^rNeT@pqeJJkxS}(vp}PgFEB%~3iUR3?Ntgf>tR_bXv(&UpH-$kgdVJk(Ut` zZ+U3;%^?VnBfdX;2~k?IsH{9~Iy54CboFm)Z8s_a%Fww;Vhe>b=%w0p3D61+nKsol z+O4!S^gKmAV!BEKthZr{wWLLjjWC3eNll@fMn;bvLpM&GYwxd`^E#OKm8QD)_x}_giAH<%A zt!_9oGS(|A_5E<3uUB~LF$INpkoSBNEDOB?nha2ydNnrQ))~1=3pq2kb(_mh)WOzR zveAs}kM0)bZCxYMgW%(@5htO=o0X34*K8YTg|8BesbpHQ1FAg2d~;YI0GX}- z5LfU%Vk_8Bc3pAL571lL=dghyh>6Q;%Iv1DeNX>5u0iCT4I%IRk!;UJyI~(`br-hN zSZEmGGh3rPjo*q~kSgEh!%FcB9}?ru^eGqUQ@ zZewGE25|Gqu<37V^zRcF=d(4xX9<55!#97CCvl!FQ94qQflF9VrQ>o-P_}pjL@#Dy zRd|T-4yr|an$Z3X-2{)(fp{?m+OpQcN(*{malsH-$JNd+A>!e z>%OC+sVl{K4F4eM!Ptf3p)mU?nZ%Q+myQvMv@tKiTwDb5LsKic2yRqFpeP9}oKrL= z`~=Q^mStGI>Z_CN|mBjMrU(*pjl{#OB0wGKvICJxaGWehw;@eg5U% zfmTA|ZFg8BJ=t4klIetq;D^l%USTYv5MHu3V!}nqz?XwwJ!WZewO4=h*SFc?#+zpq z3hYn9{1l@nP1tOxgR3G=xYA+GWNUzWcd>jfh7W=LUvsehBR)dn+bgBxJb@+}a20?Y z-`t5t6bdj@ra*QH213!z+F#bDT4_P4LJYqbfT%6gVS!84w$~#fT@cQKJk~qxO7g~elwCviG5=| zp-E8HpX_NeyWOwDLfPl7$&<0_us5`VAOR0BAPRc(!7O?o8%SX;#g|&|5(k4Ljl90b z_|S>`&FlTSUceWmt>rx_mU1aR3q+DbW|9huE_1405LXC*=jH@+PkM{BgmvdjGL5>e zFLOA3W9o2nS=WDr-o{=@Eu)C3>;_MP$<^X^wi5$2lY;FIy@?&)hpHIt!E4Z)vw{v+ zjmUxumgh7%rA3GB6ap35$SKPJ_YjOfT?12I=8TWnVfUJHjiC-wZ6B3&C4f`KpUjqtM$5sas1M4x>1MW9KT&sjQ=@Rz ztkJ!_bmwEZOpHcY9@{!^KB!rYVLR7qwb2|M8~*@u7iKMM$&6dJiKbr9CH4kERs${O zxi-V3bh?n{!1=zk^u(op=)YdW`W`mbl}gvRN*u`%Pnw=iAJ3EyWsCKQ4%?eqIl4zS zjhAKB(d9tL<43P5+}xKNEOr6(A%ur#s2;6m_LO9{6JZ1EslS%0qyBHgix`LTkZ}7Q zx%*Zh9+BvvJ6r4qO*&S)7{8QA4xQ}F|8TMR{-dKHj&U-3aIu<9Xju`O92VhFvrKX7 zUt5vdZBsrbVK-M5>F)sLyNK>sH9$}LyP*b(*h*MSvENh_vP05*7UvKn23hzj&9fN0 z?E5Qz-On%@<(_I(iyBOnb-u9^8h<-nJvktZ_~Ng{qsM-+T{QEuqW#sZ8#fw^3WTQD zI&hMg@!F12+S>UtG_PZ^)@C5 zJ(n<46OtlJo8~|o0wrxsTqV`D(i|WRdg}S=bqD-NqwV_H`qXFgoVcd2!G+xs~TN1%It1vw|23y*TgEG=YV-`(i**RUr>IXP0mpNV;F-LJIQKF`ShR4OB zkY?S6(h^sU`bS2p)V7wR*Oz}{wX$8+hsVNJT~%b(UpX(vX|B*HT_fF_PI$eiM5&c` zJ*>_lGPsbnJs{U1gX_TLc0`WD_b*8?b*ABXwSPnz?DJnfOqFhe(mb}&&cTvedQzif zyzK_X!qwSwZ{f%G$QHkDCvp*R0sJ+j8Lj5M<8&`j7Nf&4d78B$(;0+nV6hPF z-$a1a5`a#D9%RDI9W5#p`IYQIKp<3nJNare<^AvJawzA|#oO;}OmMaSnJ;Oq4ZXz9 z-t--90aH6MpOS?p=*cSJEFs%N6$1FsXOosX20k#96Er4u5HRVV#n)}T$%4uMd{~XR z0{HV5Nc2JK)GDRzm=yFuWdvr=X2Q?Zw|$JPjV`Xc5{6E)aZ2EZd8=0{j}Tmgz}XE9 zXSN041RmIk1#7Zxk|bg>Hb6QN#}ekuDo{i)wJU+5JaZpfW>937va~X}=kL+&&-ctR zm5~{bo0>0uEx@{NDX5#37efgN}nNy<4 zzcDF9G?_(gQ@KJ5f?VSIXJFSXKjv&7K&#x;(L6}c>i~u2lA6uei4VuNG=;%Zh-0JK zFCQ-@)q&J$1E>gW6)h_kMgq7&`jWty1pLtbqPI0+3dscaOAPwfq&EUObhZRdi!3jd zA=T*E#3Vr&R041j>k?ZOsd)pI0-w23|JxlJl0+I097}wQs23&Snul>54_v^du~=G3s44 z$s9~|g(EgW%EunerHUP{k9L1#4Axg$SB>t4>0~QVR_sGqa|a$I4tSI&Nb)dU{Gq%b z^enU%Q;Ym$LUlNJ@7nId(K(xGwt_M6MHPe5oizvnvl!dbx3>-Y&^C;G7Ek&Vdb_{g z_H+EA(XKPbZfvZ(AyVKpCQ?9Z#Wk6(u8Pm%ej@cVX&;4WZRsE!VnYGg*VIT>wz%66 zT^!@=pSuf{m~E!93jsV0Fd7>XvV%gS3oMg_7=2#VPR4GZA$>+?tn&(JliLn%FEJERk9%McYYhUh#?JOLpeH@&{`x@S%bpY`p3&wRc1 zg6f^GWJp^yuQZ|n%k?PDJBFcdMACva5s-7<&-OHwor8knh8W$hzSwoZaTLS}DAs-W zdx3_p%@B1QEGug$8v6?IBN}7egte-YBeMXgG^_@U=8cqxO8bS^5QQWOJ6osfvyM94 ze^ADtv)lodN(ZMV3aDfXZ$vO0Tt-XnV2@T&PQ0Pf1fRQ@eCg$?EM7Aeq{n2WN3SAi zyo4CHSpPF;p>VPdSZ)$L4sjkx4KNLtwX0i7cDfhpV_3~pO}7_(n4))jqV{EkvsH6` zo@C>&PI4BXK?S_3KTm4Kj?8f-_>`hI*gV*v9$N1rR}<98OpjWquhjS#Kw+od;RKYl zg+~LgM(M2%pX((YBy;V$G01zjJA_+W)G?i5#J1?h?4t(_vYdgw zg7nClcfKO)Q3U72d}@K?)R0)CyN*rHoH0@piV!LUeyT3qW_T`XkXr3BjnyfSB5RBn zlZizvaQ0J5i?yVutZH=7y8G=KjX{pZ$PQLRR-M}Q4H#6UA6F*u z8ZV2E2kg1bSs;XV0ro^mR;9Jl7psm}oE#-COwZG9s`0G%w@Vqnbyj_j?I!z@zL!{M zr=;EpLN9DU`2?HBQrgM+cG~bs@4dew`4KQiTYuZJ*69sbQYD@lA$%^jz=ki%*NAnm zP3>F;oTwL5UUaHs9ud0>C?9qjZ3#plFs^3Z@;);{8isbSlx1E?9p}kflu^N`Bq6)h zH`YvCFm~p;jR&!C0Yze!a%1)=TdE|ZwGM+BmMBj0Y2Tvo>=}}aj8#Gx=3wKvf68`Y z{{G(38YLP!mGg3#nisF83h2q1%BM<4r>361EUma^zdEpmzQ1;fpyF@+?m_?hU0}n* zixt1gg+>`(y3{ccb|LD*B_3ZFjrF7)g%Q00oS%@V%@-cYSR_VnIyvrL9{v`=FfET#6(xNhZ| z{gRFRJpFK({gHq2a@$H;;n6F>l7(fmaZb^2O3c4ae@vt&Wq z36&*gw!JsA$3p1I8hz0h-p$*tI7bh%U?&glJ+_~PJZ0}6 zV#FBpw(QG}98<8{593j+&9+R9xSqqnqwFLf|ALLnZ~Dv-JnfqZO~Ko=?Qw5Io@l6l zpV^H@!=zp?WEpUE`2hTaWr-NPbnTk}TtY;Q-+8Nu87o*+?OpNR-|x@^O#hR2-*CKB zxWCHCP0zgE)ppsgy)Fk5u1ugzf5+WuxU+u-E5L@QU{Y_U_jtC#y6QsB($n<=1$>^ljV+vday=HGnm_k=T7ityj;`ip!4sTa0}2{?$PJDWq6FC{D`9 z>(}hCfSc;avdG7PEfkrfEsP3EmKXT@$!WG2WHlBASsBhW?1e@WspbX;Bu5*FEn~UE z8bbW2QB*isvfeL0KmV%(xEOvaW|OVd+24ye1(Pi^q_G=k%(3-P%VfS67xHpm?!=eR zC1$Mk>|kIstj{pU|Fs*I#pmgP@&Ehlug`~a3156)&N&AC>inz*;7c#nQqSQ#s?ET> zh5ZREaK#FOLE1{q-C1oPABRr(b%Huap9oXSxOIDtB1V4?J+e+4N1lAgc6VQnJoNol z-`#T9<@91{iT*{^UNf^%d;cJf&I)hKqJ6z@`WmyhcUX=XIT(S%}U94)*p=(&TZz$oge5MIWQ!z2I znX_mBi7e>QTb4hPM&sW2kcC7hQ_xce<>1Qo(mqesHhI-Q5Kz27_55r$Xx6VDelQ&Q zzcZ07rLVu-V0*z65vBFdu$~wYV6JZbRMpQizh>q0W`BbweLHS)pHaBqRl-uy-@pIJ zdY`L{+oTx+{#FC;--&ld*x9&S4(2578fKbJ*>^>#hGj(1Cx^`2=vHpJT!wFd6#g_` zGc2#VH0*eok7!P#Vbd4%2R}~H=<91s-jBXnuut^fNO3^sE1RKx3E!;xO;b~|%DDJo z{B~Ivo|RYUFV27Y?Z5#Xlb1Y}kv8+*WzW%CM?*d^acEl0Bw=xlG*;3*jnm@bR$*$z zXm`^?=XEd}Dy(dusnAr7u@eVRZhkz0Nlju>#%lqCcNJjAG_M0}sP< zd4Z1`9)`DYym3oPAG1U|(>~Dzl5Ez)1N%z4U$j3n==<>aFx__i9QPj)O5qpi8@zIv z0f|QYL!+%BJJN7+;sN(wn~^l(y6ECNhz!E|^rVyVhUfK?4dZ#Tkn;2-12vVS&h2Y* zhH5m|R;@!#ym#gJz?or=k>0x`$|u*2)7x3h$>~bl-c_Xbz%0K zQ0J?c6CyGYS8v!_Xv3V1wb1$E+n&TR`U4b7gmhz1-?L^6zK=+jET5O)clXsqEgG6dNPme@UJQ zIg*`3B%m$Lyma%~1j)8OVLt#hKd4F#0#gZFAtWDj))mU0qF6x7M#J)aCqR)V=ih^F zG2^S&CIy&n9q`#I(AxbUot$sFZOxpL2a2==ybvI0lXgrRF^%?if3m7;#tThNdh5qj zyM-%v{)*_}%Zv_P+O1GIbi#2boP7i6;F%-eU1N^wUAWXu&PN`*Gyhhay|rMT7}hhs8<2ze(SbtQ-)hV6r`#p{Wcah0V^LCMFDKjcEVj`BCPxZ32#;x2CHgSxawI_ z;(d9vF|q~<0hZszOub!v1#R$fUNVQd#SLbow0NCGz zjFAE+c04q_LiImM9-9W1+bL7e>(M|yF@nC`^e5zfr`5L1@LZo$`)>hGKW5s95 z)mRE4-j6tnQR*m$hK4pQ`_9KqOA0+jXTc0GfwwNOOp*0%jGKRI4b{kbu4jCA0jQ?zwXHi>&4rxU18!nilnE(eG!((7fcg^RQ2h2RXYS^MajMxW&&QH88zr;NJTD_~;fzyD2nO^_Rj!tGNBCQVJf^*$rS$1}xXrawq4Mq7W>-KPiep#tI#=E*9 z3M_&h=?C0_?@{v-M@%DK7cSwS92W83CD#B`vAeleiRdg59_1}2Mg`g9743`$Tix^r zcv@Lcp-JZX&JX$G74!t_KAw<`Y)JosX73M9-g$Ztb39(FAATxf>%#z3rA?!G`bQKj z4o8e*iqHbri0||QXn|Se2o7yH$CB&=txj2^s53T|?utWaWyd5*{GmU1XRYwy1GoF= z>rk&ueF#-g6)8AS5;{It*%%c$)EZh}k*?3XJhyR&{`>nlgC&w9wO0wBuF4V>9(I%5 zloxC9u+)qS|6Ddb5GD6(zFp7NbBxY;i=48HLuDf-8E(3$9m82Cp#Gi{L$Si09~3n@ zi$X|x_+3uUDRH34ywjpN1Qk=#@2nBAtRw+St9_ozzjhD6&0*d`et!!`e;A=?(l5`n zR?jbfT%mgHF{DeifAKiSJ>)O3(`Brx`OaYLN-9wr#@}Nt9CRZ`9gTKW0#n`4{`!X!e0dCO4d$e{yv6 z7Crw?G-b&yKpLmOzKZ!93ZWLkYE7G&|;jTPfYIxf7PUHov5c1=q5_Ch0k zh3~jSjtH=p<&`Czn*J*br276Sg0|@tBXWu~K^$A}WA`G9)Rx-)Z{bUwVq5S{*}vLL zBV6GdeC~6M>}U5(bcfoXyN|(x1DS2a0Dw8pi*K#}T*$sYv!#^bYUvWGR zO?p&%&q)5BY4O4Ln$3RHRk*`;6+%ol9#4--?8-?JY55r8g390yqQ&e2BQW%oZGmKO zE>;3t9eTY^FV~gjxoylLDK|$Q^;~yTJY&e)==Fd0i4O2d?4X-w@siU}g4Scz?S5%B z(rFH~ecV{Ou5bs6hs}O5F)=D)TSF!2PP1y<**W9w3G_mzy6x;HT1%AObju6^Z0gbj zCb!;L+x(P9+vxu zMiuko6-xMfZ`;_mv+B_z$*#C}+yg91FMY8WT8q4Yy?DgTc((AZ9y%}*Ou|LA3Ilf_i5Rti;~YxRNUj~1&kS@` zi}x?eW1+d?XPeb*HnjUk*lyz+p%32~wBqFYuUn{sYv;U$R6QN@a=Odfbb*-&6C6FD zj~!hse=`&q_yWR81V1zaZItU%_7hcW2akHZgL(@0m(r^DTC`xEMQ`>uedXsNu?+h*w6cld7azU-s z_|KI%wNo&!0w~Fx58X`*2FM^nAAHtgJ~@hW_+Sr^jFiEz$GZbaW8ut7BsrrDj4)EG z>D@c=ztx3Jxh2K=7fCYrmh2~xEk*|8n=_-tyAQ;i?fGVY`nQ0q@q5eXSw5q{hoiro zXMPPdU^XLxSMS%4HoNI@OOTV4$JI+u ziSUhPE8tFcM=Kuimy%Q(AC!x^;tzrAukffc!&LL}%6gyq{09;~y@U)I&*hCjOqdC*9Alsd zTysL@wsG36!@+eUP#{F%a1KmF>M|ISb&T<@#AaNEf;(&Fe$`+0-QQ*By-uUmtm-ot znN{i1?48;0ht&e~FGxH&7c_;<26_C4pgOZar&v)$|-qdd3l~MxvhRc; zfdvSvetJ-Wv<~mlyrf-5vmU||chM4&PXH{;(9wx=J~X1gT%G zjl=EV%Z9r*goXvL(Ssq9I^Ax{tWCf-y4poNGaZX5C!ANhAtK!3VAe*+^9y*v9SYD( z6ztD874PCo z){$FJ&Qq(Q@{#PrvTLgkZSbh8c9r%=5tRVg#aq(K^jRLLL>aZOuL6>E#spfH6q3cH zkmMjY;UsIQN8{i;`V(^{L55?T0UrLa&3I+@Sn}AgD`_#huW(Q;h6I}J4@{>ZzJ_uL zx~gH-dsFIo*9XPz7i+IAA;?vkL3c7K91QPZZoxhnkWo^|_gVT9!?^syNR8l=h<8Dn zOcB*vek27G=y@!AJ<@eO92htuy&EOv9C9};T$z1<_o}JAt?fp4XT0VIadEdU^UBj^ zd%DpMp~=A1&nzRyW1~^KM{XmRq@^){YJ+g4m%pf*fBO3~B=?FrIQ%+ip3`{mESh-_ zT%S{^g+6l99&3n0OT<<_XEtX{pp?0OmJYauqc02+TPo4Hx_!*a=bHp`$9`_3jW#{;iN7n z7sa{Na}K2XO#f-&OGkB1#{b&tP@mfG$7NXf8%dhVU{ny+W&CzmiN3cjm9hGy-}-;n z-UBMCv-uxi!)y2^22FwuX(m!sL{t!@lUR@Am;Pvcj_LZ=R*>t}(Ct&-veT5*N68?{nwQGxakw{2EDIeo9(eT9B;J;xMPr zAObZ$i`SbvRV`0@A0?&Kz3QmL626fI9iy3{`T9L?PGmJa{%idi;bv(KR#3X}Ve=LU zK)F_!ywY`Y0ud4X4-sL2LZYp_QcqSAXdz^ZpKi+F8TAEqY41RQRLOMpHrL121xnB= zllgGgAO{A{zl8FZ=R5#i*QtPJWQWVF4++aySNS8W}B3%!YWkLNWG8s^*Aj zKuR490+9np$-TP(NsFI9pqLb$aM~Mmn&Nf}U;X_syCayKYu9USsDs81c_(Je|6-ds zCVagOlp_lEg1{W1_}!lg%vuy6Eq9!B@-Z7q?e&^Y1ApDj-SDVUWgx=nQ%vjKqLv0A zB@$sL!=!e9G_3lVxy*m5fiSqav3Bm+b$RebTSbVu~5J<51E?pwalr@jsN%nLi6Wo08T6Rsp z@Bp?uhmlF1LIhB-U{M>ahZ!Bcq_tgamKH%YcnyXqOJ4W+o}b^M9Oep^Lc|zMcgL-m zY9?CHhn|VGS<{b?AYPQM(&9=4wB4xSWVK+w#jbHe;wn418&+prJ*g-T-Bok!Ex_f4 zHgzijQlT^$mV(5S5`D@$KjKPS3CD%WF|a1fJC}GI36mk9K_5X=$^MpBsBF~lbRkrv zekYVh&0H{i(U_z~V}@>$u&6tCl=$Zsa4w3jDJ~5w>sPxHxsh{mfMQG1xl9Q*VrubT z0WjR2BXem6+>6?I@kutMX>2(q#lW%-Wu=ms|G%z6H!sDw%{RYh zEnb==7stVz5^yjMaX>Ntb7?o%xC$uV$zWwni=v22?ZYKtRtQf1V^`3{qMF2zR8W*2 zPeHN2*4yayPJPFW06=?>(?qJ5h3w4APY}nD%{g*F;6`J?)<<&C<8BUMHz-hV#sf|R zUDO8SF0`}q`4a`B=ac$VKT}&`nC3V@f6{GG1eCOlaz6*w;-aC`Hnz6^a3<_61gpda zf-?t$U$f4;m5BFrK~Nc_0Qe%&UUxv>(lv!Tt90HHb;asbss592WW<&KU^E0fd|=z`pBkpRI@?8 z?^<)x^;MaeuiH0m_?cY?fR9<#wFXAOSR8K~nps2wT^%KeCDJ^t39YOhKk5TldLM#b zT5~AB%_MENhKZ0NBrgR!_4q-ZZ;buWOV|k)72_V z2S!kQJF7&7jvipR`+|?jN9_dReg;1WeIc2kaqg*{VAFyozya-cEI*1{6!QLp?bJ2N zpGW9GzBvG)qTR^$#7Q?K4}9n>>d@crSb{}8i4waAOtc~&y9B=OUg?#UFZj7VIGwj_ zljKV7F3^XJ=4k~J6BMkjMl*2ec|%q`b&uWBpj+`cv;;`4to&%DQ|kp4w#@N4!nV+L zhq5^i|8sLJ@dt_u`c=YS%dRg|paP~6uItz@4kK{YrN$V!w5xs4(cA=ffJVr`m!gwP zswL1t42CZ8w4XP=@xTfiyr9YT6YhqV>sECtq`x8A4WXzC===k@U32rQaE70#857ta zp%!#TLzi#XBvhYtf&|Ac#*!9``iATDiSO@FT#JdhlzMC?^YTYtZ5!1eT=CI$D1igw z4w|DpDxwC8^CRj_Qr{8zbRV=r%?(1dfspuE+p?!*sft|q+oY2&IQbu~V7Jk<*@jBd zR}J+$l$RD8{Sl7v2g&)Sa`8D+Dxq|std}FXQd)Z1y7d zLf&G^NIiSP27we`LvPfU##d0Vj7TDAH_?V*xn?=JF2rK!I`{-~T8AO0HG2kJ3s_va z_|z_Xp1813ORaH|**4R*w1DzdVL)xuf+1-1CJB`R$gs`~L!qbwMyEAxwKvEYI+J)L zS9wXdJDPyPh$c^UADxB(p5KKq3O76lZg@m%ii3HcItC+DNmf;mX$8g$0~8)21~oWo ze|=*4gL=;&Wrr0SI?YPh^=F{QyWM>gWH16Ld9BX?F@LdENlQP2o;kHl#2^PlkHs!V zM2X~^(C-QwDgk5?YK=6tfJ?rqc&Qm%tfDOG>wtpyv)?YoelcF1D^Fulo;1CjdN4xq z#rDw<7vbi6C=|~&BS6N+KtjA-HCC-G8v@ZZu>d!e6J_Wm>QDsC$cy7qgV`}TFhFt| zGPR~6dNN3OZll@=NO+E7uNGt6S%EAOe+P|E2D*b=@TV)8` zGACI1ml3A}SM$Aj7#Ciw;~2&03kc%VebY6;bVk9aXjNz;Dv~=Wi5?7;hg#yLhc&x& z+4ez6dpn5%qjd-rgLL#7A3=CL_^zN~Uo8vZKEbUJrWRJ11{-m>o-l*iP;VM<;)FdG z78~^eW6C{R*Y<@RXQ;oV4VgrVh7I3qfF4P9ZFag~J$azU?1cWMs*a{a{T#XP3?S^y zona0BgyrzR=uSQ=`or3sot=F;AK)L^4zt%l6KmlKVhepfz0l#cMYsTic~Q;}kZMRo zoo(pshT*~p2p6uCgGDlAB8D~#-Xw;~ncXW?T5!dZH{2d~zpgDpP#Gb)11%UX9#C4BFk-oj3hfkrj z=uQVA*TCh?=Ma&-Ovry5wu#YTR)-SUTBf_tp4k(Y7Le0v%_tN`vAtOVZ94`CbZ|=t zE+V&dZnRMVuy%e9jLT|YiBrfBY9O_|O>*vhd4R|yc{!83ObiPP(+^l8-DhGQKn22D zU9iy~WswKnu=p|%S)hW#sw%~Kk%itKv>ND?A@rc;bWIoepnA{{43HJrp>P=oG#M~o z>fRPc$o~TcJDK*<>EJzgSNwX%KY0x+9y)Hddjorna7G}YXf@nnEr4j384~B^g}~m4 zBhw{5w@~QaqD?&)w)jtO%Va(cD9>G2g6?DJwG0L4W`Va7t>y!)`Aq9om6A9FKr4oz z(XIF9Ej#;K*u-sP(l7j>sCv-1oRtDM)6@T4>>gYT@(D#je#IV8i`P1`9iZsKt^^pZ z2Re9oLJ~F-8sO)5+Rrp-^%jFs-0uGSFG@=|1U}f4} z7$Y*VRO{C>4|53N=@5g#CTw);-eNH%E4^oP`e{tQFnu&o^2sW--kzq}j8dzcQAUhH zsyxtCB2a9JaPhpY3B6Lp!a|d{2iKyWnlhX^^$*>haEn4NmNfDo&SYs*iqfiRWrk#=R9DO zz1^1dSDB0eVo5Lk#W6i-Dg9p%B*e}=Hb)q)kE#Po+J zUBMN|g|yIp2x@>UknGs?2O~23Gw=}KT6F~isp;4MCB#jgW(TXtS=;>EU}hz%X#ALr z!PsvWmLNMT`ky%35PKO)0+zwvXIn%74p45AFcrBf`@%W`L&mNbZNk!v_`AD$M4&5k z8>wGFHb+fC(kOY14BgP1prrvy7(uGuSY}lw?PXp0V0@`c;*(wsg6of24!x{#stzp| zhh#5p*nP+h`HyqXTcG>2DAb#*=sbv5BAl23YnS;y@J$H|E8o~g?&GzX6v>5Dj2BF+ zFN}@{MDPn$fX|i(ONuuRl)|Lz$95O~@KCk19~Axd6(w{3FUp_lIJ?ItLm6@&@i7h@ zPj)onUUCbddd`oIJYA^^)xDiR$Ky|^J%YHKY)~N z-X`@N_Fr<=L?#~$GWXSrPfb03#+xGutVJDUEf#x0awNxzP?^oZ`tKQ=sSmNRPbYk zWA|MPGR$Li{9tH;+WXe|su!=uBKK(1<+uA*Cw^DltfA)eir0FpH$hJY*sG05Un2ki zp2s&glQElEWaLi$c1blF3nR85CUd8gylmiNfAk*(7)!n&2;Gb86QL?~D)i2;miFdm zD&H4f3nYH(MY_=xC}%U;0Of4!@E8~3K+At#s89ex9UZ|A3_6NmmRj6vQj5bFKT>}b zu(=-vVN>Pb@zdp)T_)!FfEHXVMCB<}kAFd6V;N--s*pTfcS9bFq{ADP(E;O4!m{5N zVf%g-kicJp0B-pCL&aBNpc&hOf7PkPKG7SPy-uqeQLk)8DuuEjb+0f`S&?05lJDNs zUwsmycPHo)A$!d!T_cEO z|AIE$0|YIjv)VC_b5P0=oYG^H**DwX?SCc@Kv=d_;=#?AZ_Z1Ttqy@W(QwPxaVkO? z#%GqI&EGaXrGziWs-)J<*?hIEk)g2OyZvQ=5+HSw*-v&(-2=d(KT3C@2?^Q(2tSjO zmWG(74wA!`!%)Q3U;BrrgCL05wZr|h8n(YwAMGr{`|!%fYMz6VUZ`GcBKG;9j87;p zw8!!nq04$0bFsr1GTB(_jeGoXSG`HPDT z-^=M-8aOexj}r=YUbF{_TA^NM=^YTa?1j!$m5};Lr^GVP?=OtYnt0*B_~pGrU$0UT z2beudb(+(FKR{R@Zg1a?;OR(nT|sUc4f--+VL&-xXEKa94~B6iAcD1S5Xbe)8}m5g zMF-gkzhh*6roL&gGqqQu7LZN@P}vD;RP)rKOsLKyRgnW@bocM<1wG^S+rBdZRS}5% zhu}rgNREYJE&VJs6^c2CTM{V+p&-Ig<|i0iclfJCd;7VFqh#G&Q5fuelvP z{Z7S@V8;)9#|%EcLB64WS1zSmN9gFPdZB#7)N62{%G-B#(5QoD=|$tIyms5N?34Pq z-oq^-I;+=opBO`#=3yAR^)V|GfdIq<hcj6 z$0S!+=_8*=CC@co;~Dyo5LRg92w-PWY&tJ%=oK{5IgAgiP&?$?Go|b%wv|}Oi?T(a z!;CEv)^J_sQXbpDq=u3jK8J84#XalcPh-Q;5U0Ztih;XX)#4P>x>E;(cI^XdYj6sy zQ?PaO6V&)qK3@Yxt++G%I)XP~1+9|pw7djGoN{TK{<@r?_uHY%340p{-dXg%I=>%) zL@yd)xLnWOT{@+!fC1s6|CJ=K7 zjW4>2_(7Z*=77gM#<>iLOTJ^q(9{#*Wyg-)yaaIy%ui3~%FQJ^U-=7K4I_lE-&Rj* za$F|)Es77MXLF-UKj23|S`|C@vQF>~{q!X_y+%82Fu10kq9{Z{J z-D)b2>BNP{f_%vqE-)~{e^e9B4z#lYV^~xN2)in52 z1to!?8~_zV(hD0OeN@b$AO3!9&2GYjdN5VIVRr;<<`wH4aYVa+hh&XbZ(P$>}* z28Ecvxe9z!s)6jVqXwmqDaFwY0X@_KH$a{}st4Aq-a?&N0J7=9CVFhEXy4=A$8TO@ zLj|a&O$jg_K-ly$2q>O$8Hne`=~ujXE_UN$_S8oeh(kd@>co;th&*y6&s?G{kIaU_ zb3oDQk0z=1pPlJuoWFs-G1n~l%!uJ zM8iSx*dwEC9n%u}1QE8hp{F^yHv&N&^1#@8psve)2P&Sx4EgyVW^5mVw#lR|+qc^s zo$Pr8iT1n_j-w7EA78V&OJ~hiitE?ERn5SIzR{n9CXIN^uggnb6Sd(Af-5D}?tY)q z%{Lyq%3U(dqn&wVvP)^7fZKRt2}hLCCX7&wW9-g6m{HR^Oq_%gDL?35f?}Q!bVlH< z(4Xi!4Fpiiy!PnAESpCZO92)H+b}Wr)~%z<#7^(--V&65pTe$cUm4964Lvjgc5gYf zu-4XG!Laor#bl>TS(siOI=gQ)1F8#Kq9V`aMDsP@LxD$xlEp11g#SYrHO;Sn17^Q3 z-iQSB7NoT88^gSzjBu$L>M48DTFuH*9X?)=Jq-YG$Tp$Kgs^GTV~5C`9Ld>lv$ubi zfuyO4SZKO>pS=fl--KBwTre7Z2kJC824iwL3K%e@HEp1C24EOVOlNwIB&uZkkOQ$@ zcoNSKtKpoz(iCROkRbQ@ShPa_JnyAG-l#~^CnV7Q#m=PBHA`+PX^l>6ti&8^|EU`R z&If{!9AcPn;hWv+iCYbP^`Q+Vqe-72Jr2WuJQk{1AAt+zgKnQtyBu)|=!lXM0zr^IbZHpw;0Zt}zejA4kE&4bi6GoDS#U?^{z;%!%Z zAV(4`h}@~)Q3(@dOVHB%TJv zAZBQw?2;AXE_3Qsj@l#XrMXBb>y+9V9pZcpAIN{K2Oh8qEY`)bY%62|#GhRQ`BV?B z(4)E(nUM-cpZ@zD0D9N*-R`kyNK7a(Yaxu&6XIb!qmxHWFDqtFWTL(WX`9m2{!z?q zZ$Tkao=LICgX0R%xY}>`4NC1QDDhOt-1I406g)^69&eWO9@;ekuryF>8$^ImI2AxJ z%TBv-pSeo?(QADPs~N_@$C|zaJtC1%M^ngO0jvxUe;4jv;0^IO01d#dunKdv-Yztf zW+evOY9@j6CUl1qNR@S_5XtxyC3cgC2OE__pt8vfUi?%3TG*=a+HZ`BT3kc)(JEZs zQKqZSiG=Q?mifu{&$kLrZilz&IF<;Gq&8jU%3~3vEF~c(NHS}it&is!^&`%E0+_C3 zUVI$lsHR)~#la5lZ2)b8Y!K@L4&J~YWnC8`qKa`#g+5L1Ahr(Ge9#UOyui3~?th^H z5tqT!7znxo{oO!PvHiQGiq|GDlc3PHN(rWjyZhB7+TK!>9DZ$obzug4E79?k6ElJ# za!A=3UO>@^wf2^trKr2kk3S_Mv;YeF1Vd5bN0qduKPFwwDbZFP5)sMqgd#d+wi00g zlk{6}a?dW1C6I;+^Lp6eN&;NoA(nYf&f>RFZ|>mXqx;q)hp?TcedN>ACAr8Irky_WG}T6xB4&o zcy)B=LM6KBxrY0IL63ghCg+PcOz~Qb5N=Yk9do?t(zQ-s+Wl+ zQ1Ma@{x}V+AMn0$aqqfg%{_N)lMXaJNxUFi1YJy8oh*e`f>ZQJqoD)2>Co#e)zzhz zDN-?ngLHrEIgq$c+a#uBP*ylyC-Jh!N@p4SqpQ$?fKJ9RtWRm-aH;u7gFeQj{0UlBGZWHs(0=680TNCK*UgTgKxa{1)EJ1hLYt$1a4Z9Rtm9 zK3b(+285ACUmi(|ZRUyhDTRzc3WxapqtOn4fAv~Yw8fm10-u8)PED5^3#DtrIOL^% z0Utb(iG7cc>+=<6`Bl&v7zWfH1-(k~ zv^40fU=-f1Q;jVy0C%+w)os4Ly9GAO!7O$cYc_KGB(%37oP&JIUO_qqLLs-QILIT4 zDmJN-^MRf!P1FVkLRdzC7f(P0ekGoa;CHBsGHp}Rm+(dH?YCh3gv9iyE^qr0!0MbB z=_E@Ie+!T_n8uX|K#Dd;045)GoEvI$#0VHd1`3)8u6YR+PoNZ*>N>9QgJ}$HU5CuR zH(-?^je^RMl9-&JOC{!490*fm(F@&{SMWU!*T?3d zKVU0{3D*ww;nnK}Yu?Lc~G|s9Dx{ns$IJyUnFJXtC1BlV3~| zo3Q*k=4=Sf6E8n871Xr${S+iNz2|Ltz$bFX`KtWNf9jj6N zc&o@(>7^-PiGa_50m3x%)nR3ybz5@G?1V|G(IeI)p@T?$MJR6f2${Tofvf!iAh3yMKC8K)fArX@8+8OnQLg zuwh}0z)cwKZZQ~SH$WLj6jqIF`c#1 z>WaC`i%nMv?Ff5^fzaUR<yJI)3W5 zShCCQ9Y3*0=L{PiY(P3`L>JfTfY3NyW^oNGYN{Z4JQGA1Rh6H4g^gguYC$K}uECix4go zVVp-qTMJRfyhPhlZqCG98Oct%uAxh@o9iM6DRuLxTcN@uBrNE-M<}vYYWjiw(mhrz zo(P$vK#A+}Pt#ftH-B6DrBZ2Yk;b6gzsI=tJb2BzmDtCtU&XAfn7PT`Lc#B_Czuw} zjROEW3cGv=ay|A(;g=QRb&w%cU=)^x2p+N10g+(!_$;8w92<4%+Q@6UnhdimNlf=$ zdYBW*8b;jEa|Yw)3q>w;wG8$LK--k8r4jYP_KX@dWkzm)vFmIADh)y<9cL@oB_rI^ zZFG9L&T1I>GEjAd!g4D|Xrv=lPW8q;5>Nt@CeKANSeSotEC~vo$ed0Dj@NcqI!R=k zD^6FkM8^z$e%#w^`a72CJ07O<+yAu3h@SfUXy~SvZ(`Y_*&WjbZ$*kp-*Sphw>Op@ zcQF=Mx^dyxo8}V#arArkr{0d=y5jD){kXjV_xp$6_Oku_yKokf$dK^n$Dd2_B<;qD z-lNG@x2~0sJ_tKekUdNK-S^)8`&zj*rovjeYcdj|luBlwx)`kpzUn(~o#^`1I~%b@ z;7|)DJH;d}Bce8kVe5eOHjSu=Nqx4Dr?zGEDG_k@q5D8(9iHo zv5KiWWh2sy;|@1x2d*(aYK+0LN-uR^BLeEbY7Z-`$-+IqfVD55|1i(5HABi<9$jkH zT{@ixc&FjOoLAA2ynHZEoozap@cQVDi^DKaDy?fhYXI1=K}j&<7p83*Rg;cee>mVf zcI4Ix1_jXNkX>0yL(C{%!+HiMI-f@-D01#O81xOMT8+-m&31x{tgwOt{?2A{YW%s* zF1<3l=QP>3pn++p$%;k6%F)jFw%yYRV@28~XbC8H*#)eC^uEH?9oQ8s^E5TOr1Bu54CRk3ab4 z>&N*&)7N62@Vl`;%ysRrx=s}d>}(B5>U0(DeaKha-l`P?jP&U=T$#TUssyR4aOZO9 zW0WrCcUvB8erI;&%>DZcgJZ`9VKQfSze>ml7>1Z?{XGl-p#sB!y=Et}dmhT$-cu5f z#j^8w9h`V&lB!v@2EulKaJ2-w6p?O3_7C6ZJlrt#LYGt@ht&<}Y zmBYHEn-LWux`;6L7Gd1FfBxUv*-kiU87VH}w$$5fz0i+qx(r*zu-1b`bmS!GhhLy% zD)U7}ttbX_EBay9kY;QH4TEEJf@*@qtr}s{2DjG9$Y4#}R%U87f2fLwr`Ny-tE!3u zR^ja?&s_?C(y*yHJ@L!)$17rEPSJF25g5I1K4md%E3drO`cOD(O=!Xc;+@(@j{!VGUSyLkv|FlJmmB|kpS-^VL7g}W^3p4&=;ZrdH z7hIjJ*2R1V4Y!sX%^A0}67Lm2H<&}xZe)5kHVgZjF8yvDWC`Wj%ygdMq& z{Gk{2FUSzBsfP4{=gO|flArPOAsKR%R)#$0veU6R%`JWs=H#Kp>|~bRVOO*4v}JV_ z*_x~{y1Q*D(R)X$xngjjAk1CJu^QNJ;$FMLBgR6SG^*wNhji)1nh(LcpDqsM^z(}@ z_HqieV8&YNl%<14{r?>jcR^Jl2*gISQ)9htRGba}{Djvk~6TubUtkIW@B)Qu}$v5Rm&S_ANn9n)Gd>#DwQ^?AuymfTNlG>? zP0tI8?pUYm{J^!19ntbPXu|>)$i%)nd)4yMQQCT_Tj~6WUU%~)d$4M zy>5LS(ea0a>VB3aksL^Fr1*vY&V?Xr8#}+sO_-sxw3rV2x%<-P&&~zFb{QM5$kz9| zv9Og{8JGj_n6v(@`Jd(rVJb5o8cg;18$CSEhJU}!aJaQo$CR_BXlm+qnFvgLSr^5y zq_^`Qp-npm$XP zVgA2^5UE^0jszcuJ)hX-Ivid!&pVlAUiq%)p)~=&!vs#Ttr{9KE)#)* zE$Tvc>!z7EW3`|$PA9P}ul$+#Vi!Ayf9Z_Tsc-GQbXyZe&kXs6X8kjuBQw=nsqBygv30h1o7)LNzr*|>JdPY*Nmj2=zN?Zi z4yZAGBT_Y~pirggigm1-$@=3R$wlfVq*7d9o6DqA(Vls?_EF28qFq@o?p*u!?W1V^ z;P>ll4&x!di_`kNty!yDO83XI!)4{%3ZMuC%t1&rgWe4ffF51h;%1@zwEqLDeUcqM&PK#UlQOQm}RmOeLl46&Qa+Wl`8G zDa#&$N!RNlUNOa!NS#%XNr^RPNH7636fQj%AL~rW>WsA}xHD`m1@yyZFv}*ODKk+s z_o&mnsax>fv|a~V%gn=QnTxt*^0%0}Oucu;e^e37wlPV(*GTH-G%OPDxL^u}OHEc# zbO0}iR)vNpdw6>G{{e%W?&U26EOkB7J>4hH#43Dg#u^=u z2HW>Ma2^!z#6dUH)|uEp{`ljWd4BLrwlQ^$nBrs_+id^%Jyx^QfrPW8i|;<%VbC^}Mt+r;P$tv7*~&fv@-goCeVmh%%8e{WeJX&3egAz|k%yPp0Ps}loo!N= zW3B?asOos5wu&id*C5ljZG$jhp%Mzr{Sp%s<%J2Y=F(Lz5#y$QB?buBZHSaFqr+3Px=_-91k`vk{%uYP_XODrT0Il z+*|X}b-aLD*w@myNp63&gzgb8GDOX}I2X>o|Ni;&7ZnetxGdIKqBvQ4R>uJ|GnV{) zXOO{f@9%laNr8pWLcAlr|MDhGbq84d>SCK7PSJ6Z{EeqHrkpWj375l__oP_4!%8!X z?}1rDa~$5U*)sWsLVUQZa;^&*le~Yj%jG`l)AcbGX!{0}-vN^~6jlj=)-(0%J?z}2 zgxT}h`+QA*s8OX!cAz1)^X#!$y#j)d_LbY5Jeh{c8e&xO-=*s0gx$QEzrbVi&Hf96TP_7s1uY zomD6eQYI&+|yb{GNDvr~8e~*Y=a=|A4$#kUV_#RJShV>)< z!%;aMt=n!U*hfARDhco$#^#?fQ->n8%23fb*=I=}>-hd&JXc0^7Z`xmEZJTES-qT| z^Qh8{g4|XGOT7;r8S1x;u;#oOhjGkzwcxV+Yixr~9>1Y+T0C7fg&~$&VA$l_>%(^O zHu(Wph}<#lgiU9`h8bSFG>}onu~Hep{Z512`Kw z4#W~N$HCd8xj)9WQU*J`V+=Nh`DXFzn`>0I7roEpe^Klk_g^EpMPYy4##R;1y1rxw zzlBQZskdx<;Q5(V_u;;ilb5m?=aZW_{0>y7^|w@^466=(N+S|KKwj$p%v8ZPzse>) z;ae?p50_m^xczhNR4{t!OM7wB&%EF(V&L8I1+6@Hbs7?3L9c@i_TwH7<~o)QY)=%# zRmjDio3ZcS394;vcx6}kO3|LYhE7f8HmrwQ%I5Vqq1V`UhL?JuNiMRE*f(dfbkCFh z{lu&G(+uY;MX+t~?9d_n|32dy8k6WLUfm6^zR<-eeOZhF!xPBpqy54l#TrbI>Z4Q| zyQiVW8QXbsDfld1#9o(+cv^A5&2g(k%G+(aSnzCju zx?iz^#cdgV-d0dF7K%q#`R2C&HmMwhgMmAfkP3=jg^~zw!EGSnrQTnJ#tBhar@AT>qpaQ z1dqlu5Z(;EoAL|Ls;;c8%9uG-GuE2AjWGfV*{_1cVaM0(T&EmxUy!NNOcA=fdk6$C zY2~4{2a>2sIh40{K|+9D)MVDWMjhuA@qSig46(ODOb?jb4)$3$?oT#VfQ zA%(uON&GlZxj5ySONcRB6sSy$9w&bbn|4iQ@k86t@2Py1@`%?*JJ3LO_{XkYblix+ zuwUiEam#TWV25Y44g`;S{kCv1_wU})2nLMat9MekbOl-X3ws&WVQVRP%B6s1n`<-D zZ4OMEvB#QofiMuH$S&oz&5qXABCCtNyW`fa7-L5CBRY5<2FXbU7$|ga?J9zCYSeuX zx9Ee^aj<2li*H2fmZu=>xrqEJO@AtRdEQ*%rvEgz9+!na$WDfZmJXfdOv5Uf+ zG48_Dq!GM9mzcB1-PCScVflBA$*9t}(OpSg1dtax7e8aT`CR|L2#}K)nG$;IOn%ys zrYpxzr~SA=tjDvh@Lt8#V$~Fqn-2x8X}u#?%jJ{y<9dL8iu?g}X%pvw@jT^-TKv4P!|Z_Xx)%HhpJo}n=1uVU$?4xNgcfp`pU8SCOvjFZq_H>oYyl=(u{}ssP z2J|g*kuSW1ms}}y{lnApK+$AtuxQo}IoAVnIe!L~__XQ1-+Ji=?UNk&jUu8?RJn{3+NzR`Cgd$$u&r2+on|l(z?n8ZUCjZK z^ioa3J&-^Lc|)8nAyP)O*78cOkaJ1lw;2mI#{W6?KyW*)3Dd_#&#|{;(!$0W!?&ju}X4-ra^rZXNsW1tmB`7cVoNmCG;$RfG z{0c{p{ghraUV#v8y{9E{FuxFI?uq& zFUtDn|H>MgUqzzmWbAO5uQEz)Y2dO`OJGnH8w|8sCG6Nwc5nlv$+AWjS=Zm*)+QRfBP#?(60n zU6m!pOaf=t#OT>YpK_&RgPTK$MGUrtXZ&(zXDwB*w~$)h9a$+w98>f^7EZSTw#FF` zHrTr{_LhMo>VIde*qik~A9?RAiOU1VwQWVkJIf1$;C!tg#(Z{M8Lw8DL}-uaQcFBZ zlYYqwG)w-F+4bj^B-`ol*x~$+l!Nd|KO zC%LvzFJ#Dw$@0s&VO_B0iy2ie*tw?)0Y`!rXWV?+0{eK8&fHCgzN$}@VVX5eNDPNp zh>n(EipUTBo)7zxuc`Tkfy!p_^LGLNxwH^M&Tw;G;e><$%2Q6uV5@kM1RG29cAX0o zg((=+U6im3+;;aIeJ$j-T%iaC5hbzV*MWe>Ugh9R|^_*M`PX zNzEsEhi7GFL3(TD$mj5XYM_Z-9+V0pyPm;n$Qc%&v{2Yxkt{MP!rT1Z;I-*iaLWpp z7G`I0D7tS{b$FfhV7FKymQ0f%4!#Jj!{hP&@bb#L?{n&iZC<_pbD3cI89TXPFjdAN z^#5#|a~H!xnDk;;?Sa)Z;GbLiT%c_wv{#-B;GjN)JTeK5v|S}6<{&9 z^ryQK(`N%9Oy@6ZWe92X zwZlElQXk7IFAG1jn-7%xi_Vq{CWAPE5c|_g+6NE0N!^j)Q(bE1QLglc8GR+Tc7BLL zEltoA2){+T5f%^OBrsl%+_`hdp_jGuc}GXbH#RFb^w?#ta4ZXi+xd?wDv^e>fub31 zm4RbJ!=SI<*IvM2JXi&6cU=K|G{8!K*t*ql0&?)M7YwWBm7`WJVc+j1F>|)62kz;< z({bD`*GFQ}xXg|tNRYSWux`f;dy@)WB8UqHu}kMXl7c!2H0$$W+1e8a+f*$r4C_M9 z2V{ur`=^(0DV$2F|fH`HmT|f8Y=(sPt3 zXDdzOD?+w{0f(sB28HpO>-Lws#albQy^N#}@dnKqgNA?{kV8(%e=zwc*4$?IgWA?L zr!}CNJVh4n_Ddu6@h*%uCkv{*Z+?-^I#?{hjEw|baPc8OKYy?hx1O|)v=@;A{-0=>-0b7ffQ ziCtDHDRKwRt^&Hzc#E)iDsE}!N$H~mz%UH!U}Uq?E`y?#l|xTLw6A@#avI!p2%*zm zTIY$Tx_U>Vg}&iojf*rpnes(VI>#zh<>LsRZBabWM`Ssj{498R8Rf^<55*txhJue^ zWOBSHq3sYfCKBRRJ`CigIjkRxAI^gOY>`O=O2=Tq>0dd9tW{uesg@Kz>X77U46-X} zuN+_M!L2Y8Ez5txFywht&kA|a>{67hRwQCRCJ8sDj-=G z;Y+)@?N9SShRh3U5i3zfftC}{jW0#e_^9&YLxEOFdLb@g@Kqb~OeQNYe#=;w2Q^Z; z!{=_y&7tu1`k@yHMb_6lYGmQ5s>*wgo79TfnSJ*13qE`KZqDm3zoq@R!9btQ#4!~y z26y3$=0dgNM;_RRy3UY}3@gFG(tWcL zkgNi5uTaMp zdMqJhw>Z9$Jk1I3rk}<#(9^!XtG%P`bipTlv`8B@G_$t;uG2#6na-(@N{QL>K!*W; znhQWAd`CJ;xnwm2iXDHXBqhoi>|nSvPA3S6B+t9Xgwm@J-y<9nP=U5E-I%L}Yrv-G zfuNPFfv@8E{PO%uv{k*K>{0Pqw^1!sPuQ z8ow0G2&Qx(y6bF|v^k+J4* zADS~Dz;lzR@7>>F5*PK^o}@jC{xe;Ygzit(PoT!wwS6?HtB^SvnxB+Q0em+rQ(k@T zl~T?&sO$GLvIY>s3Q<*~0bN3OnJ@mpO{bD*PLYWm3Xqnbg<8EWYOx@4 zRtwO1zNY*dS!t^rOLxlWKkSd95o2_JY$*cm^#g~C^#Hq-;sDeQAl#K0O)3IRa&J>; zXY8?cTe@>(R=faE;^u}pl@6&vdsA*uNMp&)4g)Z9!l8uC#{o1Wh_+e{+LJKxAJ}yl zFRp-bieJlZh^}%p@ol<`oxgj7k?nCaG_(|j|IojnVh*+E<1RlT=@MHMb+b>PXfsWO z7y@JA6Yr3OzNaL4yaMO5CZWH0!_{B9*U}k3G^gs>DRvlzpnD<|PHvS;rDm=~yNgSB z9nDD?%tNPv=@P3dRdv;k6PTSU^y#hzXvdp)$HjrFDvzQ^f-c7U>$XW8K~Jm!Q^r6Z zA=iwVZacQrdN;&Z0RWW0^TzzV;rj=?Y0fL|E&YW*`>EwLt?NVkM`neb=JL7IkB+%a z_W&++(Z6bW^OoBB_A6o#yrmwYy{6jfaoC`jDoS!zGkx-D)Ia^(r_g_W>Lae!__^11 zdR@)&8CG>*7ep~I3K_YX}l2G*|uchvExSLBwmqw2zkm-=yhU1)s#k zXOlvaN)>Knt+~0&=VYl=Dv2MJo@rx z@2c0p4=aCW&8*z$8;->e!c6I{^#2bWAM{aS18j+jFE3U&9AyxB^twE#;VT<@03XNU zuRSFEU9ZY>qRqcGZ|bgm+Rh+n%NHsUmy+lR_~na?zt*8&3_jA|&Qf)Rui?Lt(_2>V(OQug_6%IBdf9jEvXgJkf`TR`k&&DEnTC5oL z5A>h%gB=C{hOL#VZv& zgOR0QF3nE~M#6Dg>Nq9xtJ5qeM&4{p{?u=X2eT$0aFS1uUE6KI9uX(BfgAY$@8#ze z1Y%81njHpJ(>B6@Z#Kq&k_gFvt36Xb?OPr4V)^ClcN=09^^Gyggu@inJbQPzI&o{2 zRf@-N#7HwX+TgWOF#xwXxb1+dr18dSUiW7oX|J3gZ3Xj4p*y?k3{31IUN5gH_^ys-`lL?sAuQefxMscJ8SFlYzDRnG+_FyxeHmAMf`e%1rb%7NpZv9tZ{E(gP zIohC_BzR+>{E)=(>kaX2`v$i}8d&JaXhvef#dgDDB6>q&!gkzCz~#WcDhU-NCMu&h zA|}Ls+nAUzVQq|!HZWT3De|}Kd$J*$oA=FzR;|<0OK9^U0@xIY5@e?X2xM+ZNPO+6 z{%&JJ-_Mp9?x&WPI@V^z*GdzH`IqavE1lmMle`01Uz;tC+t;+hrGWLAOU^`xQ3(+N_;W74ZX^#o)w9&ZfEx_?uyn%Oap7|hD?L5U7ll2PdJ^s zw}8MG!+%tKaWrfrvU7;*t9w4bP&oUR)*0C|l$5GWYX&idEUGL$a~7m%m2}=lr09^) zzxjactdL(gpMiWbE>Afh80X4e5v5Y+ylge{QTJ0u@fI?E&~0LK-g3BWT*s6n3dV}9 zn2G+`RZI9+c1}fkQE~#^z5~NyPl1wD@ZFVZD9B)mwA_7`@uIAa& zGQ{m}DEeauK1SV8^2ZvQn`b&X)s2lR*y-w`PwkC_G4Bv!ZdUwcvo7J+PCLSUM90(z zd$^>gK8m|g;OHj9qJy+C+ZMldbO)RmT1OYo`u#p}TLXgLwIK&7l+CMEX=yon;aN60}O{OA8K&-V`L7)OWFJKy|+S!m} z&HQh18R~kh-v+g&8x78nJ1lnJCw!P}tgrKd5ck|1Au=mZ8lt_VK>~?1>^e~*nf1v6 z-Z7XbIklkVWLQw8$<(m2deU)yXc;NBG_|3fy@54N#XKXAvj{HFS`~X3xh-c@L68#C4}>1w9q@=wH)lOX26!PG zf}}$w$V z45=c$7<6+{wuH+S*c9Be_RFp*^JbaI0q4?;8I({p0_gH=R6hI#5vTRgsXWk3OEvb0ddIlZj3z z_pqv{>$-iP3kZsQ|H$YZ$*K-ZgJXr2-AnKRz|5nqM`U<}MiByt!^1kI* z^-H$4>;+%)#c=FP{bB=S6_tCQ>;W(JOAIfo)JCt7C zaI{AA53lx?a7veW65o>)0`JIroVcqC+P5oGJ-R+=WHv;=AR1aU1iI9#Zj3@3SV{KI zc6RpRiJ{Wv!4S98lj`@G%AnyPnxC~Ri0mr;Dnqn@SE>B3fq_f+w@MbFlHRVnX&p}H zEg>#xk7Grp305q%)#pn$;OhtC-d_Kj3nudu z{>4dh=qdam)5Q=1%sOk59WRDp-zMTsz}TW?SAj2*g0Eu&KYf30ha?`w!gK(h4S)xE(Su>FdM! zJ2YS4*xQNwI-dj%d!?igo^^6+(b94obMnp1{K4F;vJ~gzt*a7|{mJ#i(;sf#g!aA% z-T$h=U%8xnUN`TP@R8 zi*rBRFjWK!@c>l8{+w{9i`{&@mU?MJj9s31SmZgrjZkRu&e!=S-=wa+=E=O5SBms( zd0DQYap7McFQg>l7G-aS>jZDMqIvxg(*@N?UfQiuFpah>g7cTKxgzpj=rfgLv? z4e-vvYO7gC;4MNs;Z@b>+j{h7m;{ZMyIvgaEhJ5WbUWz#^9PupG^aK?TJ~1J`Q_tS zc^8+Ru0tm`vX^DtF7kghy{P84nzcR~gf>;b zr|mJU_@H6#r1i@N%$DlTOJ!dT6xxeQUb)gd+vkJ+6IhH9bGF|D(^P>L%G#b#M_JyM z!2B$+ZcbSe4RhKQinvHPdGKi;DV2v=*%^T z{Ve{VGAeP0PG_ ze0cbPIKz;UmyK=2t@^hqv6}c-RriF14NVmkUTA983c0x!r==LcPiLy0KgnEN{P`x7 ztr`nMN&Hr`cCeM&_~J@IOa7krznFIAZ=up(-^4$da)P5+4pR;#?{6EtY-Hp`A~rWK zJPgJ!RT~KqVhBSuNqIcC_lRZ!usB}Zf1FP}x68q1sM^C{MBp4hKTB&ZHez8`FfE*0 zf1pAE{(yxA4|%J_UcS)O;Lxb6+q41uWlr|%YH|c7`?{Pwc~3|v5G$P*AIS^MB?`0q z^kLmiFHa6jS?_OW+!p-Ki&R}D07D0c&U?9#bJMekPgB0>9~QObx)rS+c?&5rYX#6cdJ8*;aD_1C#P>2Y-mDY>W+6zsiPPm!hN9 z1nob*`GMp3$ZL{+PJ8?6st1|~Rh&7K@CYTYL(AbEqP9clGsVSft@=vkOc<2nG<9`h zDu*-ruc6a%bc+-#m`uPf$I`<43q>c|tOQ?WWW3e{A!iqzHy8eKaS_avxw1U8%{O8{ z&Z&Q${*|Y|HZ-~n_WkkWK~JUQ)3d~7VTmqa(Aiq|4gJATGMPu7eL!SjtR<1E+~x)+ z?lAgjj=!G6#48R~1`R{Q5v_?_;^wOR;0jDX+IfBH@4GDNx33n%T+Bp35dBtg|NpV~ z-eFN*TjS^?p6HiYh%tf)LSk1C6a=K37$tNSr56=pM4Hs0gE5MVfQo>W0Tcm|&d|G! z&Y-lBI!GIO?=#HYwFf=X{7w$qIW28@curBZeeb$ z7Fk*ssYSu^b0byvmgsN~J?{Iql;pLvq%X8m$zJ@~}@D7ap=;jovp1;0U)Z^{r zJ9;?>eMzPU1#!6RI-WyG18r&1caxm83}!TzTX3EZ+4R}!Ck`StA=S>$}?;tNdN zX5|ei8F+MBEBNR2>%p+O8}87Qu-FbM%*I6GKuf-aLQAXN#j%b}a|Y0Qi?!e#3C1ja zHl~R}xLvd{oZDpsKbh>!ULMbI|@JCO8v~sIoDa< zVi>)clXSqq(sE#EK=L|uwHgOe+VKf!`xcC*Y&DsL21?w(FiG_NA8SQm)iIXr`X3(J z6$BmOm1GtrJT=%%G!oA7J1|35dQ9V2KF3>fK(cZ-H9R=4s#*~)nbJ=H2RE|GlC!dS zt;}7I<7@Bjh?@^PdI+?;X8LqJT4s+#2r1mEbbL3!q;6{^fNGBD8_wmNHerBKQw}zp^Ier4fHR{vudz z+%mQox0+VZ-x{MVF4i@6L?Bq9OVK7g&7a}sL`mlDD3s1DD$-!>>J;m5A?*npEf9q@ z^@;?}6*j=aI`{h^g~X=f{vElYM1+3juW*am5zz`$^LROpcO6(|E+*XDpN>zc8n@{d zacj%DqnFxQR@6#q4NI1FwFO}9A?{8di}d#Fm&q_zkb;&A_hx7RDEg|PUA$me8PRy`VNtD7XT|J%;p} z=aZ}fXtx)Cs_5q(4hRyec?FXqCT3D5B3Rx9S&ApbnEm150&ChO^LSDrnTtkGG|0X+G1kRk?#;MU5zmi1>Pl`_hK7 z4&n@qWrO9?rPCcQ88UrC;$mH0Oh}Hs+3s*p55x+!zr|fyYy~#h41KDvTb%XgsFkD` z>R~#WJ6ArLUfrO&e-K5Wv=$7z0_pO0koJJ1o7S0SDVbxUj7-C8dRb4p>9prbdi8{1 z?W;4>1Ue*^x0so-OK}ivM)|PpE{fGf}tvHZG8#nE~`0CGZL#{9vgcsDnp+d}e?CtEU8F-hzYmJ08`S}68x`|`7 z_Xhd5*T)$F2d_w0;IBdJ8H~?S6tNh7r=puB7$9755Uown^b*8^vd}lUy=9|k6VeBg z^S^7A9>eH_xQfBjyfS%qa8Sgy?#Uf!pB~ULek0f|j@JK{wwMdL42@?gO;(q;mcVtP*Fyfnt{6)z^d z$l6`RoC(RfsCeFWW$He7cV#Y!t30($WT!6)A7?W=9PCajXD$uA%Q>`0+`_3MAub`I z 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

eh#xA!pu~x*VkJ%MBl@eIIq{ zHA1V|2kv({fF{{_GpgqZBq)tW+-;;<7ik2t;$_Q+`y8_gj8b}BVXVFnJ9P3_I7m|Vr^p}aJ0~GhaCs1#`*2fkhEm35H_g2 z0*53Bkt5NfPpkGJU}vJsYL&B4Etv8b`7=8rE@O3qM*GvM+Us$>aX|x+Yv{Xm47^t; zj&!<9?qqAS&I*&|Sfu5G;YRKPDy4Wk8HgN1{I|mgp!J+06Kae5x4tf$5sUWpY2!3N zM6rjv2ulLjFcz6n!c49<^}% zt-AaAtd=!y@NJi4(&gJ>t_a%+Oe6H!bymOx?)p;ugt$FE{&rYBVsrAoksEd2E!@+n z3VAq)(1uMKjLE;w(+4?+942H@vla_++>|-$6@n=aWX-Z;N2l2bAt#|(Gtp4a`yN_` zqbVo(sD;DF__(4|zS~G`&sYR%&8^6tdR10d%9dW;j&iv4E3?blN@Njoe>E~jE5;{H%vwAO8$ zmIu@sV=S+~9X1(-S&1uDF3i97Q;Jnwh3R!b-H}*XPNTir!hyd8sE|=@$rTq0=0HCAwn=jg3k)W zqVKv=XAggO$<e0JtO!FRRB?*aqO$?F!VT#+=js6*Ll|S=VziS^da~zrs85f z%2-{j)CL@{xc+y3TlQHGbKP3m|L`CG zKd}MoQOH+J2`c?CKtNXO7Z4M{l=C0e{!UnGwSk2ldJMy2d}gp3Ow}A;NSRk|NNMr zi5lJipFjCG|8dt`u5j^h{(8s9*M-RSAv%8YZ~m+Q|D~|;%@VzK;omILn Date: Sat, 5 Nov 2022 17:26:46 +0100 Subject: [PATCH 169/208] added scheduling visualization to notebook --- .../notebooks/Quantization_Visualiztion.ipynb | 3467 ++++++++++++++++- 1 file changed, 3448 insertions(+), 19 deletions(-) diff --git a/examples/notebooks/Quantization_Visualiztion.ipynb b/examples/notebooks/Quantization_Visualiztion.ipynb index e88609a..52ffbbf 100644 --- a/examples/notebooks/Quantization_Visualiztion.ipynb +++ b/examples/notebooks/Quantization_Visualiztion.ipynb @@ -2,12 +2,49 @@ "cells": [ { "cell_type": "code", - "execution_count": 40, + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/paul/uni/deeplearning/bitorch/venv/lib/python3.10/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from bitorch.quantizations.quantization_scheduler import MixLinearScheduling, StepScheduling\n", + "import torch\n", + "from torch.nn import Tanh, ReLU\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.animation import FuncAnimation\n", + "from IPython import display\n", + "from typing import List, Callable\n", + "\n", + "from bitorch.quantizations import Sign, SwishSign, SteHeaviside, ApproxSign, ProgressiveSign, InputDoReFa, WeightDoReFa, Identity\n", + "from bitorch.layers import QActivation\n", + "import numpy as np\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantization Visualization\n", + "\n", + "Use the code below to visualize the quantization functions supported in bitorch. The upper graph shows the applied quantization during forward pass (black graph) as well as the integral of the backward pass function (red). The graph below shows the function applied during backward pass." + ] + }, + { + "cell_type": "code", + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "

" ] @@ -19,14 +56,6 @@ } ], "source": [ - "from typing import List, Callable\n", - "\n", - "from bitorch.quantizations import Sign, SwishSign, SteHeaviside, ApproxSign, ProgressiveSign, InputDoReFa, WeightDoReFa\n", - "from bitorch.layers import QActivation\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from sklearn.inspection import plot_partial_dependence\n", - "import torch\n", "\n", "\n", "LABEL_POS = -0.4\n", @@ -40,11 +69,8 @@ " # initialize a numpy array with the correct size\n", " numeric_gradient = np.zeros(len(x_values) - 1)\n", "\n", - " # TODO: calculate the numeric gradient between each consecutive pair of x values based on the given y values\n", - " ### BEGIN SOLUTION\n", " for i in range(1, len(x_values)):\n", " numeric_gradient[i-1] = (y_values[i] - y_values[i-1]) / x_step_size\n", - " ### END SOLUTION\n", " return numeric_gradient\n", "\n", "\n", @@ -57,15 +83,11 @@ " # intialize an array with the correct size\n", " y_values = np.zeros(len(x_grad_values))\n", "\n", - " # TODO: construct a list of y values for a (representative) function that would create the given gradient values\n", - " # HINT: start with any arbitrary value, create y_values step by step, finally subtract something from all values so the middle element becomes zero\n", - " ### BEGIN SOLUTION\n", " y_values[0] = 0\n", " for i in range(1, len(x_grad_values)):\n", " y_values[i] = y_values[i-1] + x_grad_values[i] * x_step_size\n", " y_values -= y_values[len(x_grad_values) // 2]\n", " y_values += y_offset\n", - " ### END SOLUTION\n", " return y_values\n", "\n", "\n", @@ -79,7 +101,7 @@ " axes = [axes]\n", " # create the actual plot\n", " for fn, axis_idx in zip(functions_to_plot, range(len(functions_to_plot))):\n", - " x = torch.tensor(x_values, requires_grad=True)\n", + " x = torch.tensor(x_values, requires_grad=True, dtype=torch.float)\n", " y = fn(x)\n", " y.sum().backward()\n", "\n", @@ -110,7 +132,7 @@ " first = True\n", " for fi, fn in enumerate(functions_to_plot):\n", " fn.training=True\n", - " x = torch.tensor(x_values, requires_grad=True)\n", + " x = torch.tensor(x_values, requires_grad=True, dtype=torch.float)\n", " y = fn(x)\n", " y.sum().backward()\n", " c = \"grey\"\n", @@ -161,12 +183,3419 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantization scheduling visualization\n", + "\n", + "Use the code below to visualize the scheduling of quantizations. You can replace the used quantization functions and observe how they are applied with different factors." + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### " ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "steps = 100\n", + "factor_samples = 5\n", + "function_samples = 300\n", + "\n", + "\n", + "FIGURE_SIZE = (8, 10)\n", + "LABEL_POS = -0.3\n", + "xlim = (-1.5, 1.5)\n", + "ylim = (-1.3, 1.3)\n", + "\n", + "def sample_scheduler(i, axes, scheduler):\n", + " \n", + " axes[0].clear()\n", + " axes[1].clear()\n", + " \n", + " x_values = np.linspace(*xlim, steps)\n", + " x_step_size = (xlim[1] - xlim[0]) / steps\n", + " scheduler.factor = i / steps if i <= steps else 1.0 - ((i - steps) / steps)\n", + " x = torch.tensor(x_values, requires_grad=True, dtype=torch.float)\n", + " y = scheduler(x)\n", + " y.sum().backward()\n", + "\n", + " y_values = y.detach().numpy()\n", + " \n", + " numeric_gradient = calculate_numeric_gradient(x_values, y_values, x_step_size)\n", + " numeric_grad_function = reconstruct_gradient_function(x.grad, x_step_size, y_offset=0.0)\n", + " axes[0].locator_params(tight=True, nbins=3)\n", + " axes[1].locator_params(tight=True, nbins=3)\n", + " axes[0].set_ylim((-1.3, 1.3))\n", + "\n", + " axes[0].plot(x_values, y_values, color=\"black\")\n", + " axes[1].plot(x_values, x.grad, color=\"black\")\n", + " axes[0].plot(x_values, numeric_grad_function, color=\"darkred\")\n", + " axes[1].set_title(f\"factor: {scheduler.factor:.2f}\", y=LABEL_POS)\n", + "\n", + " for axis in axes:\n", + " axis.axvline(x=0, c=\"lightgrey\", zorder=0)\n", + " axis.axhline(y=0, c=\"lightgrey\", zorder=0)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "scheduler = MixLinearScheduling([Identity(), Tanh(), Sign(), InputDoReFa(2), InputDoReFa(4), InputDoReFa(8), ReLU()], steps)\n", + "fig, ax = plt.subplots(2, 1, figsize=FIGURE_SIZE)\n", + "anim_created = FuncAnimation(fig, sample_scheduler, frames=2 * steps, interval=50, fargs=(ax, scheduler))\n", + "\n", + "video = anim_created.to_html5_video()\n", + "html = display.HTML(video)\n", + "display.display(html)\n", + "\n", + "plt.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "scheduler = StepScheduling([Identity(), Tanh(), Sign(), InputDoReFa(2), InputDoReFa(4), InputDoReFa(8), ReLU()], steps)\n", + "fig, ax = plt.subplots(2, 1, figsize=FIGURE_SIZE)\n", + "anim_created = FuncAnimation(fig, sample_scheduler, frames=2 * steps, interval=50, fargs=(ax, scheduler))\n", + "\n", + "video = anim_created.to_html5_video()\n", + "html = display.HTML(video)\n", + "display.display(html)\n", + "\n", + "plt.close()" + ] } ], "metadata": { From 33ed93bf68fed0bf8b9102e953ac087471f8ee2b Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Tue, 8 Nov 2022 14:09:26 +0100 Subject: [PATCH 170/208] made code compliant to pep 484 --- bitorch/layers/__init__.py | 4 ++-- bitorch/layers/debug_layers.py | 10 +++++----- bitorch/layers/extensions/layer_implementation.py | 8 ++++---- bitorch/layers/extensions/layer_registration.py | 4 ++-- bitorch/layers/extensions/layer_registry.py | 4 ++-- bitorch/layers/pact.py | 4 ++-- bitorch/layers/qactivation.py | 2 +- bitorch/layers/qconv1d.py | 10 +++++----- bitorch/layers/qconv2d.py | 10 +++++----- bitorch/layers/qconv3d.py | 10 +++++----- bitorch/layers/qembedding.py | 8 ++++---- bitorch/layers/qlinear.py | 6 +++--- bitorch/layers/register.py | 5 ++++- bitorch/models/base.py | 4 ++-- bitorch/models/densenet.py | 6 +++--- bitorch/models/lenet.py | 6 +++--- bitorch/models/meliusnet.py | 2 +- bitorch/models/resnet.py | 6 +++--- bitorch/models/resnet_e.py | 4 ++-- bitorch/quantizations/progressive_sign.py | 8 +++++--- bitorch/util.py | 6 +++--- examples/dlrm/datasets/base.py | 2 +- examples/dlrm/facebook_dataloading/data_utils.py | 6 +++--- examples/image_classification/datasets/base.py | 2 +- examples/mnist/datasets/base.py | 2 +- 25 files changed, 72 insertions(+), 67 deletions(-) diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index e5e975d..26051ef 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -3,7 +3,7 @@ and activations before forwarding them. These layers use the quantization functions specified in the quantization submodule. """ -from typing import TypeVar +from typing import Optional, TypeVar import torch from torch import nn @@ -49,7 +49,7 @@ T = TypeVar("T", bound=nn.Module) -def convert(module: T, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> T: +def convert(module: T, new_mode: RuntimeMode, device: Optional[torch.device] = None, verbose: bool = False) -> T: """ Convert the given module to a new bitorch RuntimeMode. Needs to have custom implementations installed. diff --git a/bitorch/layers/debug_layers.py b/bitorch/layers/debug_layers.py index a3bc19b..cabc8b6 100644 --- a/bitorch/layers/debug_layers.py +++ b/bitorch/layers/debug_layers.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Optional, Any import torch from .config import config @@ -49,8 +49,8 @@ def _debug(self, debug_tensor: torch.Tensor) -> None: class _GraphicalDebug(_Debug): def __init__( self, - figure: object = None, - images: list = None, + figure: Optional[object] = None, + images: Optional[list] = None, debug_interval: int = 100, num_outputs: int = 10, ) -> None: @@ -69,7 +69,7 @@ def __init__( self.set_figure(figure) self.set_images(images) - def set_figure(self, figure: object = None) -> None: + def set_figure(self, figure: Optional[object] = None) -> None: """setter for figure object Args: @@ -77,7 +77,7 @@ def set_figure(self, figure: object = None) -> None: """ self._figure = figure - def set_images(self, images: list = None) -> None: + def set_images(self, images: Optional[list] = None) -> None: """setter for images list Args: diff --git a/bitorch/layers/extensions/layer_implementation.py b/bitorch/layers/extensions/layer_implementation.py index 222102d..73ffa16 100644 --- a/bitorch/layers/extensions/layer_implementation.py +++ b/bitorch/layers/extensions/layer_implementation.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Any, Tuple, TYPE_CHECKING +from typing import Optional, Any, Tuple, TYPE_CHECKING import torch @@ -35,7 +35,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: Optional[torch.device] = None) -> Any: """ Create a new layer based on a given layer recipe (can be expected to be from the default category). @@ -61,7 +61,7 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: return True, "" @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: Optional[torch.device] = None) -> Any: return cls(*recipe.args, **recipe.kwargs) @@ -77,5 +77,5 @@ def can_clone(cls, recipe: "LayerRecipe") -> Tuple[bool, str]: raise NotImplementedError("A custom layer should implement their own compatibility check.") @classmethod - def create_clone_from(cls, recipe: "LayerRecipe", device: torch.device = None) -> Any: + def create_clone_from(cls, recipe: "LayerRecipe", device: Optional[torch.device] = None) -> Any: raise NotImplementedError("A custom layer should implement a method to create a cloned layer.") diff --git a/bitorch/layers/extensions/layer_registration.py b/bitorch/layers/extensions/layer_registration.py index bcc118a..33652c8 100644 --- a/bitorch/layers/extensions/layer_registration.py +++ b/bitorch/layers/extensions/layer_registration.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Any, Type, Union, Tuple, TYPE_CHECKING +from typing import Optional, Any, Type, Union, Tuple, TYPE_CHECKING import torch @@ -100,7 +100,7 @@ def supports_mode(self, mode: RuntimeMode) -> bool: def can_create_clone_from(self, recipe: LayerRecipe) -> Tuple[bool, str]: return self.class_.can_clone(recipe) - def get_replacement(self, recipe: LayerRecipe, device: torch.device = None) -> Any: + def get_replacement(self, recipe: LayerRecipe, device: Optional[torch.device] = None) -> Any: return self.class_.create_clone_from(recipe, device) def is_default(self) -> bool: diff --git a/bitorch/layers/extensions/layer_registry.py b/bitorch/layers/extensions/layer_registry.py index 0f7820d..c6b3407 100644 --- a/bitorch/layers/extensions/layer_registry.py +++ b/bitorch/layers/extensions/layer_registry.py @@ -32,7 +32,7 @@ def get_recipe_for(self, layer: Any) -> Optional["LayerRecipe"]: return None return next(filter(lambda x: x.layer == layer, self._instance_recipes)) - def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe, device: torch.device = None) -> Any: + def get_replacement(self, mode: RuntimeMode, recipe: LayerRecipe, device: Optional[torch.device] = None) -> Any: layer = self.get_layer(mode, recipe) return layer.get_replacement(recipe, device) @@ -110,7 +110,7 @@ def convert_layers_to( self, new_mode: RuntimeMode, only: Optional[Iterable[Any]] = None, - device: torch.device = None, + device: Optional[torch.device] = None, verbose: bool = False, ) -> None: for recipe in list(self._instance_recipes): diff --git a/bitorch/layers/pact.py b/bitorch/layers/pact.py index aa9ee87..c2d204f 100644 --- a/bitorch/layers/pact.py +++ b/bitorch/layers/pact.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Optional, Tuple from torch.autograd import Function from torch.nn import Module import torch @@ -42,7 +42,7 @@ class Pact(Module): Choi, Jungwook, et al. "Pact: Parameterized clipping activation for quantized neural networks." (2018) """ - def __init__(self, bits: int = None) -> None: + def __init__(self, bits: Optional[int] = None) -> None: super().__init__() self.alpha = torch.nn.parameter.Parameter(torch.tensor(10.0)) self.bits = bits or config.pact_bits diff --git a/bitorch/layers/qactivation.py b/bitorch/layers/qactivation.py index d21f8f6..c6722c0 100644 --- a/bitorch/layers/qactivation.py +++ b/bitorch/layers/qactivation.py @@ -56,7 +56,7 @@ class QActivation(nn.Module): def __init__( self, - activation: Union[str, Quantization] = None, + activation: Optional[Union[str, Quantization]] = None, gradient_cancellation_threshold: Optional[float] = 0.0, ) -> None: """initialization function for fetching suitable activation function. diff --git a/bitorch/layers/qconv1d.py b/bitorch/layers/qconv1d.py index 48dcaf2..94d0bdb 100644 --- a/bitorch/layers/qconv1d.py +++ b/bitorch/layers/qconv1d.py @@ -1,6 +1,6 @@ """Module containing the quantized 1d convolution layer""" -from typing import Any, Type, Union +from typing import Optional, Any, Type, Union from torch import Tensor from torch.nn import Conv1d, init @@ -24,8 +24,8 @@ class QConv1d_NoAct(Conv1d): # noqa: N801 def __init__( self, *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, + weight_quantization: Optional[Union[str, Quantization]] = None, + pad_value: Optional[float] = None, bias: bool = False, **kwargs: Any, ) -> None: @@ -82,8 +82,8 @@ class QConv1dBase(QConvArgsProviderMixin, QConv1d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, + input_quantization: Optional[Union[str, Quantization]] = None, + weight_quantization: Optional[Union[str, Quantization]] = None, gradient_cancellation_threshold: Union[float, None] = None, **kwargs: Any, ) -> None: diff --git a/bitorch/layers/qconv2d.py b/bitorch/layers/qconv2d.py index 3b57d0d..e01f06d 100644 --- a/bitorch/layers/qconv2d.py +++ b/bitorch/layers/qconv2d.py @@ -1,6 +1,6 @@ """Module containing the quantized 2d convolution layer""" -from typing import Any, Type, Union +from typing import Optional, Any, Type, Union from torch import Tensor from torch.nn import Conv2d, init @@ -19,8 +19,8 @@ class QConv2d_NoAct(Conv2d): # type: ignore # noqa: N801 def __init__( self, # type: ignore *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, + weight_quantization: Optional[Union[str, Quantization]] = None, + pad_value: Optional[float] = None, bias: bool = False, **kwargs: Any, ) -> None: @@ -76,8 +76,8 @@ class QConv2dBase(QConvArgsProviderMixin, QConv2d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, + input_quantization: Optional[Union[str, Quantization]] = None, + weight_quantization: Optional[Union[str, Quantization]] = None, gradient_cancellation_threshold: Union[float, None] = None, **kwargs: Any, ) -> None: diff --git a/bitorch/layers/qconv3d.py b/bitorch/layers/qconv3d.py index e976b02..623e872 100644 --- a/bitorch/layers/qconv3d.py +++ b/bitorch/layers/qconv3d.py @@ -1,6 +1,6 @@ """Module containing the quantized 3d convolution layer""" -from typing import Any, Type, Union +from typing import Optional, Any, Type, Union from torch import Tensor from torch.nn import Conv3d, init @@ -19,8 +19,8 @@ class QConv3d_NoAct(Conv3d): # type: ignore # noqa: N801 def __init__( self, # type: ignore *args: Any, - weight_quantization: Union[str, Quantization] = None, - pad_value: float = None, + weight_quantization: Optional[Union[str, Quantization]] = None, + pad_value: Optional[float] = None, bias: bool = False, **kwargs: Any, ) -> None: @@ -76,8 +76,8 @@ class QConv3dBase(QConvArgsProviderMixin, QConv3d_NoAct): # type: ignore def __init__( self, # type: ignore *args: Any, - input_quantization: Union[str, Quantization] = None, - weight_quantization: Union[str, Quantization] = None, + input_quantization: Optional[Union[str, Quantization]] = None, + weight_quantization: Optional[Union[str, Quantization]] = None, gradient_cancellation_threshold: Union[float, None] = None, **kwargs: Any, ) -> None: diff --git a/bitorch/layers/qembedding.py b/bitorch/layers/qembedding.py index 2f97f9e..53b1811 100644 --- a/bitorch/layers/qembedding.py +++ b/bitorch/layers/qembedding.py @@ -17,8 +17,8 @@ def __init__( self, *args: Any, embedding_dim: int, - weight_quantization: Union[Quantization, str] = None, - output_quantization: Union[Quantization, str] = None, + weight_quantization: Optional[Union[Quantization, str]] = None, + output_quantization: Optional[Union[Quantization, str]] = None, **kwargs: Any, ) -> None: super(QEmbeddingBag, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore @@ -88,8 +88,8 @@ def __init__( self, *args: Any, embedding_dim: int, - weight_quantization: Union[Quantization, str] = None, - output_quantization: Union[Quantization, str] = None, + weight_quantization: Optional[Union[Quantization, str]] = None, + output_quantization: Optional[Union[Quantization, str]] = None, **kwargs: Any, ) -> None: super(QEmbedding, self).__init__(*args, embedding_dim=embedding_dim, **kwargs) # type: ignore diff --git a/bitorch/layers/qlinear.py b/bitorch/layers/qlinear.py index 968c49a..3670764 100644 --- a/bitorch/layers/qlinear.py +++ b/bitorch/layers/qlinear.py @@ -1,6 +1,6 @@ """Module containing the quantized linear layer""" -from typing import Any, Type, Union, Dict +from typing import Optional, Any, Type, Union, Dict import torch from torch.nn import Linear @@ -18,9 +18,9 @@ class QLinearBase(Linear): def __init__( self, *args: int, - input_quantization: Union[str, Quantization] = None, + input_quantization: Optional[Union[str, Quantization]] = None, gradient_cancellation_threshold: Union[float, None] = None, - weight_quantization: Union[str, Quantization] = None, + weight_quantization: Optional[Union[str, Quantization]] = None, **kwargs: bool, ) -> None: """Applies the given quantization functions on weights and inputs before applying the linear operation. diff --git a/bitorch/layers/register.py b/bitorch/layers/register.py index 904f412..9e447d9 100644 --- a/bitorch/layers/register.py +++ b/bitorch/layers/register.py @@ -27,7 +27,10 @@ def all_layer_registries() -> List[LayerRegistry]: def convert_layers_to( - new_mode: RuntimeMode, only: Optional[Iterable[Any]] = None, device: torch.device = None, verbose: bool = False + new_mode: RuntimeMode, + only: Optional[Iterable[Any]] = None, + device: Optional[torch.device] = None, + verbose: bool = False, ) -> None: """ Convert all wrapped layers (or a given subset of them) to a new mode. diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 367d2f0..7616d0d 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from typing import List, Any +from typing import Optional, List, Any import torch from torch import nn @@ -82,7 +82,7 @@ def initialize(self) -> None: elif isinstance(module, nn.Linear): nn.init.xavier_normal_(module.weight) - def convert(self, new_mode: RuntimeMode, device: torch.device = None, verbose: bool = False) -> "Model": + def convert(self, new_mode: RuntimeMode, device: Optional[torch.device] = None, verbose: bool = False) -> "Model": return convert(self, new_mode, device, verbose) diff --git a/bitorch/models/densenet.py b/bitorch/models/densenet.py index 971832a..3db4b17 100644 --- a/bitorch/models/densenet.py +++ b/bitorch/models/densenet.py @@ -53,7 +53,7 @@ def __init__( reduction: List[float], bn_size: int, downsample: str, - image_resolution: List[int] = None, + image_resolution: Optional[List[int]] = None, dropout: float = 0, classes: int = 1000, image_channels: int = 3, @@ -154,7 +154,7 @@ def basedensenet_constructor( dilated: bool, flex_block_config: Optional[List[int]], classes: int = 1000, - image_resolution: List[int] = None, + image_resolution: Optional[List[int]] = None, image_channels: int = 3, ) -> Module: """Creates a densenet of the given model type with given layer numbers. @@ -237,7 +237,7 @@ def __init__( bn_size: int = 0, dropout: float = 0, dilated: bool = False, - flex_block_config: List[int] = None, + flex_block_config: Optional[List[int]] = None, ) -> None: super(DenseNet, self).__init__(input_shape, num_classes) self._model = basedensenet_constructor( diff --git a/bitorch/models/lenet.py b/bitorch/models/lenet.py index 41fdae2..9f09934 100644 --- a/bitorch/models/lenet.py +++ b/bitorch/models/lenet.py @@ -1,5 +1,5 @@ import argparse -from typing import List +from typing import Optional, List from bitorch.layers.debug_layers import ShapePrintDebug from bitorch.layers import QLinear, QConv2d, QActivation from torch import nn @@ -18,8 +18,8 @@ def generate_quant_model( self, weight_quant: str, input_quant: str, - weight_quant_2: str = None, - input_quant_2: str = None, + weight_quant_2: Optional[str] = None, + input_quant_2: Optional[str] = None, ) -> nn.Sequential: weight_quant_2 = weight_quant_2 or weight_quant input_quant_2 = input_quant_2 or input_quant diff --git a/bitorch/models/meliusnet.py b/bitorch/models/meliusnet.py index af4525b..cc163ea 100644 --- a/bitorch/models/meliusnet.py +++ b/bitorch/models/meliusnet.py @@ -83,7 +83,7 @@ def __init__( bn_size: int = 0, dropout: float = 0, dilated: bool = False, - flex_block_config: List[int] = None, + flex_block_config: Optional[List[int]] = None, ) -> None: super(MeliusNet, self).__init__(input_shape, num_classes) self._model = basedensenet_constructor( diff --git a/bitorch/models/resnet.py b/bitorch/models/resnet.py index b9912ab..b570a6b 100644 --- a/bitorch/models/resnet.py +++ b/bitorch/models/resnet.py @@ -1,5 +1,5 @@ from .base import Model, NoArgparseArgsMixin -from typing import List, Any +from typing import Optional, List, Any from bitorch.layers import QConv2d_NoAct import torch import argparse @@ -417,7 +417,7 @@ def __init__( layers: list, channels: list, classes: int, - image_resolution: List[int] = None, + image_resolution: Optional[List[int]] = None, image_channels: int = 3, ) -> None: """Creates ResNetV1 model. @@ -467,7 +467,7 @@ def __init__( layers: list, channels: list, classes: int = 1000, - image_resolution: List[int] = None, + image_resolution: Optional[List[int]] = None, image_channels: int = 3, ) -> None: """Creates ResNetV2 model. diff --git a/bitorch/models/resnet_e.py b/bitorch/models/resnet_e.py index 870c10f..32c1722 100644 --- a/bitorch/models/resnet_e.py +++ b/bitorch/models/resnet_e.py @@ -3,7 +3,7 @@ `_ paper. """ from .base import Model, NoArgparseArgsMixin -from typing import List, Any +from typing import Optional, List, Any import torch import argparse from torch import nn @@ -172,7 +172,7 @@ def __init__( layers: list, channels: list, classes: int, - image_resolution: List[int] = None, + image_resolution: Optional[List[int]] = None, image_channels: int = 3, ) -> None: """Creates ResNetE model. diff --git a/bitorch/quantizations/progressive_sign.py b/bitorch/quantizations/progressive_sign.py index bdeb0ce..63591ef 100644 --- a/bitorch/quantizations/progressive_sign.py +++ b/bitorch/quantizations/progressive_sign.py @@ -77,8 +77,8 @@ def __init__( use_global_scaling: bool = True, initial_scale: Optional[float] = None, custom_transform: Optional[Callable[[float], float]] = None, - alpha: Union[int, float] = None, - beta: Union[int, float] = None, + alpha: Optional[Union[int, float]] = None, + beta: Optional[Union[int, float]] = None, ) -> None: """ Initialize the progressive sign module (can be used for progressive weight binarization). @@ -113,7 +113,9 @@ def current_scale(self) -> float: return self.scale @staticmethod - def default_transform(scale: float, alpha: Union[int, float] = None, beta: Union[int, float] = None) -> float: + def default_transform( + scale: float, alpha: Optional[Union[int, float]] = None, beta: Optional[Union[int, float]] = None + ) -> float: """Transform the given scale into the temperature of the progressive sign function with the default function. The formula is as follows: 1 - (alpha ** (-beta * scale)) diff --git a/bitorch/util.py b/bitorch/util.py index 86e95c3..92a2aa3 100644 --- a/bitorch/util.py +++ b/bitorch/util.py @@ -1,15 +1,15 @@ # import sys import typing import importlib -from typing import Callable, List, Any, Dict +from typing import Optional, Callable, List, Any, Dict @typing.no_type_check def build_lookup_dictionary( current_module_name: str, class_strings: List[str], - filter_by_superclass: Any = None, - filter_fn: Callable[[Any], bool] = None, + filter_by_superclass: Optional[Any] = None, + filter_fn: Optional[Callable[[Any], bool]] = None, key_fn: Callable[[Any], str] = lambda x: x.name, ) -> Dict[str, Any]: """Builds a lookup dictionary based on a list of strings of class names. diff --git a/examples/dlrm/datasets/base.py b/examples/dlrm/datasets/base.py index f006b59..d79500b 100644 --- a/examples/dlrm/datasets/base.py +++ b/examples/dlrm/datasets/base.py @@ -19,7 +19,7 @@ class BasicDataset(Dataset): num_train_samples = 0 num_val_samples = 0 - def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + def __init__(self, train: bool, root_directory: Optional[str] = None, download: bool = False) -> None: """initializes the dataset. Args: diff --git a/examples/dlrm/facebook_dataloading/data_utils.py b/examples/dlrm/facebook_dataloading/data_utils.py index 61e195e..a425288 100644 --- a/examples/dlrm/facebook_dataloading/data_utils.py +++ b/examples/dlrm/facebook_dataloading/data_utils.py @@ -44,7 +44,7 @@ import logging from os import path from multiprocessing import Process, Manager -from typing import Any, Union +from typing import Optional, Any, Union # import io # from io import StringIO # import collections as coll @@ -425,8 +425,8 @@ def process_one_file( split: Any, num_data_in_split: Any, dataset_multiprocessing: Any, - convertDictsDay: Any = None, - resultDay: Any = None + convertDictsDay: Optional[Any] = None, + resultDay: Optional[Any] = None ) -> Union[None, int]: if dataset_multiprocessing: convertDicts_day = [{} for _ in range(26)] diff --git a/examples/image_classification/datasets/base.py b/examples/image_classification/datasets/base.py index f006b59..d79500b 100644 --- a/examples/image_classification/datasets/base.py +++ b/examples/image_classification/datasets/base.py @@ -19,7 +19,7 @@ class BasicDataset(Dataset): num_train_samples = 0 num_val_samples = 0 - def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + def __init__(self, train: bool, root_directory: Optional[str] = None, download: bool = False) -> None: """initializes the dataset. Args: diff --git a/examples/mnist/datasets/base.py b/examples/mnist/datasets/base.py index c48bf18..d291fe4 100644 --- a/examples/mnist/datasets/base.py +++ b/examples/mnist/datasets/base.py @@ -19,7 +19,7 @@ class BasicDataset(Dataset): num_train_samples = 0 num_val_samples = 0 - def __init__(self, train: bool, root_directory: str = None, download: bool = False) -> None: + def __init__(self, train: bool, root_directory: Optional[str] = None, download: bool = False) -> None: """initializes the dataset. Args: From 7cef061b3b237c50e6a0c8e6c614c8fc26efca0c Mon Sep 17 00:00:00 2001 From: Maximilian Schulze Date: Thu, 10 Nov 2022 16:39:45 +0100 Subject: [PATCH 171/208] Add implementation of BEmbedding and BEmbeddingBag --- bitorch/layers/bembedding.py | 237 +++++++++++++++++++++++ tests/layers/test_bembedding.py | 328 ++++++++++++++++++++++++++++++++ 2 files changed, 565 insertions(+) create mode 100644 bitorch/layers/bembedding.py create mode 100644 tests/layers/test_bembedding.py diff --git a/bitorch/layers/bembedding.py b/bitorch/layers/bembedding.py new file mode 100644 index 0000000..a5b205a --- /dev/null +++ b/bitorch/layers/bembedding.py @@ -0,0 +1,237 @@ +from typing import Union, Optional, Dict, List, Tuple +import numpy +import torch +import warnings +from bitorch.layers.config import config +from bitorch.quantizations.base import Quantization +from torch import Tensor, nn +from torch.nn.parameter import Parameter + + +class BEmbedding(nn.Module): + """Binarized version of pytorchs embedding layer. Uses given binarization method to binarize the weights. + Memory consumption during training increases with batch size. Inference is always small. + """ + + def __init__( + self, + num_embeddings: int, + embedding_dim: int, + padding_idx: Optional[int] = None, + weight_quantization: Union[Quantization, str, None] = None, + device: Union[str, torch.device, None] = None, + sign_bool: bool = False, # Whether a boolean 0 represents a -1. Set to True for Sign. + ) -> None: + super().__init__() + # Load the quantization function + self.weight_quantization = config.get_quantization_function(weight_quantization or config.weight_quantization) + # Random initialize the weight. Can be set using set_weight. + self.weight: Union[Parameter, Tensor] = Parameter( + torch.rand((num_embeddings, embedding_dim), device=device) > 0.5, requires_grad=False + ) + + self.padding_idx = padding_idx + self.sign_bool = sign_bool + + self.optimizer: Optional[torch.optim.Optimizer] = None + self.optimizer_dict: Optional[Dict[str, List[Tensor]]] = None + self.unique: Optional[Tensor] = None + self.unique_vectors: Optional[Tensor] = None + self.out_param: Optional[Tensor] = None + + def select_unique_vectors(self, flat_indices: Tensor) -> Tuple[Tensor, Tensor, Tensor]: + """Given a flat tensor of indices, return the unique indices, their inverse in the original tensor, + and a tensor with embedding vectors that are indexed by the unique indices. + + Args: + flat_indices (Tensor): A flat tensor of indices that query the embedding table. + + Returns: + Tuple[Tensor, Tensor, Tensor]: unqiue indices, inverse indices, unique indexed embedding vectors + """ + unique, inverse_indices = self.unique_wrapper(flat_indices) + unique_weight = self.weight.index_select(0, unique).to(torch.float32) + return unique, inverse_indices, unique_weight + + def unique_wrapper(self, tensor: Tensor) -> Tuple[Tensor, Tensor]: + """Compute the unique values and inverse indices of a given tensor. Uses numpy when on cpu and otherwise pytorch. + + Args: + tensor (Tensor): Tensor to compute the unique values from. + + Returns: + Tuple[Tensor, Tensor]: unique values, inverse indices + """ + if tensor.device.type == "cpu": + unique, inverse_indices = numpy.unique(tensor.numpy(), return_inverse=True) + unique = torch.from_numpy(unique) + inverse_indices = torch.from_numpy(inverse_indices) + else: + unique, inverse_indices = torch.unique(tensor, return_inverse=True) + return unique, inverse_indices + + def apply_padding(self, indices: Tensor, embedding_vectors: Tensor) -> Tensor: + """Applies padding to the embedding vectors. Sets the embedding vector to zero where + the given unique index matches the padding_idx property. This operation is inplace. + + Args: + indices (Tensor): Indices of the embedding vectors. + embedding_vectors (Tensor): Embedding vectors to be padded. + + Returns: + Tensor: Padded embedding vectors. + """ + if self.padding_idx is not None: + embedding_vectors[indices == self.padding_idx] = 0 + return embedding_vectors + + def transform_zeros(self, embedding_vectors: Tensor) -> Tensor: + """If the sign_bool property is set, replaces 0 with -1. This operation is inplace. + + Args: + embedding_vectors (Tensor): The tensor to be modified. + + Returns: + Tensor: The modified input tensor + """ + if self.sign_bool: + embedding_vectors[embedding_vectors == 0] = -1 + return embedding_vectors + + def set_optimizable_weights(self, weights: Tensor) -> None: + """Inject the weights to be optimized into the optimizer. + + Args: + weights (Tensor): The weights to be ioptimized. + """ + if self.optimizer is not None: + if self.optimizer_dict is None: + self.optimizer_dict = {"params": [weights]} + self.optimizer.add_param_group(self.optimizer_dict) + elif self.optimizer_dict: + self.optimizer.state[weights] = self.optimizer.state[self.optimizer_dict["params"][0]] + del self.optimizer.state[self.optimizer_dict["params"][0]] + self.optimizer_dict["params"] = [weights] + + def forward(self, input: Tensor) -> Tensor: + """Generates embeddings for received tokens. + + Args: + input (Tensor): indices for embedding + + Returns: + Tensor: embeddings for given token + """ + input_shape = input.shape + self.unique, inverse_indices, self.unique_vectors = self.select_unique_vectors(input.flatten()) + self.apply_padding(self.unique, self.unique_vectors) + self.transform_zeros(self.unique_vectors) + self.unique_vectors.requires_grad_(True) + out = self.unique_vectors.index_select(0, inverse_indices) + self.set_optimizable_weights(self.unique_vectors) + return out.reshape((*input_shape, -1)) + + def set_weight(self, weight: Tensor) -> None: + if weight.dtype != torch.bool: + weight = self.weight_quantization(weight) == 1 + self.weight.copy_(weight) + + @torch.no_grad() + def step(self) -> None: + """Step the BEmbedding by copying the optimized unique embedding vectors into the binary embedding table.""" + assert self.unique is not None and self.unique_vectors is not None, "Call forward before step." + if self.padding_idx is not None: + self.unique_vectors = self.unique_vectors[self.unique != self.padding_idx] + self.unique = self.unique[self.unique != self.padding_idx] + self.weight.index_copy_(0, self.unique, self.weight_quantization(self.unique_vectors) == 1) + self.unique = None + self.unique_vectors = None + + def set_optimizer(self, optimizer: torch.optim.Optimizer) -> None: + """Set the optimizer to set parameters to be optimized dynamically during training. + + Args: + optimizer (torch.optim.Optimizer): The optimizer of the `BEmbedding`. + """ + self.optimizer = optimizer + + +class BEmbeddingBag(BEmbedding): + """Binarized version of pytorchs embedding bag. Uses given binarization method to binarize the weights. + Memory consumption during training increases with batch size. Inference is always small. + """ + + def __init__( + self, + num_embeddings: int, + embedding_dim: int, + padding_idx: Optional[int] = None, + weight_quantization: Union[Quantization, str, None] = None, + device: Union[str, torch.device, None] = None, + sign_bool: bool = False, # Whether a boolean 0 represents a -1. + mode: str = "mean", + ) -> None: + super().__init__( + num_embeddings=num_embeddings, + embedding_dim=embedding_dim, + padding_idx=padding_idx, + weight_quantization=weight_quantization, + device=device, + sign_bool=sign_bool, + ) + self.mode = mode + self.embedding_dim = embedding_dim + warnings.warn( + "The BEmbeddingBag is experimental. Using the BEmbeddingBag leads to significal slowdowns of the model." + ) + + def apply_aggregate( + self, batch_size: int, offsets: Tensor, inverse_indices: Tensor, unqiue_embedding_vectors: Tensor + ) -> Tensor: + """Aggregates the unique embedding vectors using the defined mode. + + Args: + batch_size (int): Batch size of the input data. + offsets (Tensor): Offsets of inverse indices for each batch. Defines which embedding vectors are aggregated. + inverse_indices (Tensor): Flattened bag of indices for each batch. + unqiue_embedding_vectors (Tensor): Unique embedding vectors to be aggregated. + + Returns: + Tensor: The aggregated embedding vectors. + """ + out = torch.zeros((batch_size, self.embedding_dim), device=self.weight.device) + for row, (start_index, end_index) in enumerate(zip(offsets.tolist(), offsets.tolist()[1:] + [None])): + use_indices = inverse_indices[start_index:end_index] + if self.mode == "sum": + out[row] = torch.sum(unqiue_embedding_vectors.index_select(0, use_indices), dim=0) + elif self.mode == "mean": + out[row] = torch.sum(unqiue_embedding_vectors.index_select(0, use_indices), dim=0).div_( + len(use_indices) + ) + elif self.mode == "prod": + out[row] = torch.prod(unqiue_embedding_vectors.index_select(0, use_indices), dim=0) + return out.reshape((batch_size, -1)) + + def forward(self, indices: Tensor, offsets: Tensor) -> Tensor: # type: ignore + """Generates embeddings from given tokens and offsets. + + Args: + indices (Tensor): The tokens to be embedded. + offsets (Tensor): The offsets describing the starting points of batch items. + + Returns: + Tensor: The embedded and aggregated tokens. + """ + self.unique, inverse_indices, self.unique_vectors = self.select_unique_vectors(indices.flatten()) + self.apply_padding(self.unique, self.unique_vectors) + self.transform_zeros(self.unique_vectors) + self.unique_vectors.requires_grad_(True) + batch_size = offsets.size(0) + out = self.apply_aggregate( + batch_size=offsets.size(0), + offsets=offsets, + inverse_indices=inverse_indices, + unqiue_embedding_vectors=self.unique_vectors, + ) + self.set_optimizable_weights(self.unique_vectors) + return out.reshape((batch_size, -1)) diff --git a/tests/layers/test_bembedding.py b/tests/layers/test_bembedding.py new file mode 100644 index 0000000..0214fb0 --- /dev/null +++ b/tests/layers/test_bembedding.py @@ -0,0 +1,328 @@ +import numpy as np +import pytest +import torch +from bitorch.layers.bembedding import BEmbedding, BEmbeddingBag +from bitorch.quantizations import ApproxSign, Sign, SwishSign +from torch.nn.functional import embedding, embedding_bag +from torch.optim import SGD, Adam + +TEST_INPUT_DATA = [ + (10, 10), + (100, 10), + (1000, 100), + (30000, 300), +] * 3 +TEST_QUANTIZATION_FUNCTIONS = [ApproxSign, Sign, SwishSign] +TEST_OPTIMIZERS = [Adam, SGD] + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +def test_bembedding(vocab_size, embedding_size, quantization_function): + qembedding = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + example_input = torch.zeros(vocab_size, dtype=int) + example_input[np.random.randint(vocab_size)] = 1 + + output = qembedding(example_input) + + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + + raw_embeddings = embedding( + input=example_input, + weight=binarized_embedding_table, + sparse=False, + ) + assert torch.equal(output, raw_embeddings) + + # now sparse tests + qembedding = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + + example_input = torch.tensor(np.random.randint(vocab_size), dtype=int) + output = qembedding(example_input) + + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + raw_embeddings = embedding( + input=example_input, + weight=binarized_embedding_table, + sparse=True, + ) + assert torch.equal(output, raw_embeddings) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +def test_batched_bembedding(vocab_size, embedding_size, quantization_function): + qembedding = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + example_input = torch.zeros(vocab_size, dtype=int) + example_input[np.random.randint(vocab_size)] = 1 + example_input[np.random.randint(vocab_size)] = 1 + + output = qembedding(example_input) + + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + + raw_embeddings = embedding( + input=example_input, + weight=binarized_embedding_table, + sparse=False, + ) + assert torch.equal(output, raw_embeddings) + + # now sparse tests + qembedding = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + + example_input = torch.randint(0, vocab_size, (np.random.randint(1, 100), 1), dtype=int) + output = qembedding(example_input) + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + raw_embeddings = embedding( + input=example_input, + weight=binarized_embedding_table, + sparse=True, + ) + assert torch.equal(output, raw_embeddings) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +@pytest.mark.parametrize("optimizer", TEST_OPTIMIZERS) +def test_bembedding_training(vocab_size, embedding_size, quantization_function, optimizer): + example_input = torch.randint(0, vocab_size, (1,), dtype=int) + example_output = torch.rand(size=(1, 1)) + assert_equal_train(example_input, example_output, vocab_size, embedding_size, quantization_function, optimizer) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +@pytest.mark.parametrize("optimizer", TEST_OPTIMIZERS) +def test_batched_bembedding_training(vocab_size, embedding_size, quantization_function, optimizer): + batch_size = np.random.randint(1, 100) + example_input = torch.randint(0, vocab_size, (batch_size,), dtype=int) + example_output = torch.rand((batch_size, 1)) + assert_equal_train(example_input, example_output, vocab_size, embedding_size, quantization_function, optimizer) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +@pytest.mark.parametrize("optimizer", TEST_OPTIMIZERS) +def test_batched_bembedding_training_duplicate(vocab_size, embedding_size, quantization_function, optimizer): + example_input = torch.tensor([0, 1, 2, 3, 1]) + example_output = torch.rand((len(example_input), 1)) + assert_equal_train(example_input, example_output, vocab_size, embedding_size, quantization_function, optimizer) + + +def assert_equal_train(input, output, vocab_size, embedding_size, quantization_function, optimizer_class): + input.requires_grad_(False) + output.requires_grad_(False) + weights = (torch.rand((vocab_size, embedding_size)) * 100) - 5 + linear = torch.nn.Linear(embedding_size, 1).requires_grad_(False) + below_zero = False + if quantization_function()(weights).min().item() < 0: + below_zero = True + qembedding = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + sign_bool=below_zero, + ) + qembedding.set_weight(weights) + model = torch.nn.Sequential(qembedding, linear) + optimizer = optimizer_class(model.parameters(), lr=0.03) + qembedding.set_optimizer(optimizer) + model_output1 = model(input) + torch.nn.functional.l1_loss(model_output1, output).backward() + optimizer.step() + qembedding.step() + model_output1 = model(input) + torch.nn.functional.l1_loss(model_output1, output).backward() + optimizer.step() + qembedding.step() + + class NormalEmbedding(torch.nn.Module): + def __init__(self, weight, q_function) -> None: + super().__init__() + self.weight = weight + self.q_function = q_function + + def forward(self, x): + return embedding(x, self.q_function(self.weight), sparse=False) + + normal_embedding = NormalEmbedding(torch.clone(weights).requires_grad_(True), quantization_function()) + model = torch.nn.Sequential(normal_embedding, linear) + optimizer = optimizer_class(model.parameters(), lr=0.03) + model_output2 = model(input) + torch.nn.functional.l1_loss(model_output2, output).backward() + optimizer.step() + model_output2 = model(input) + torch.nn.functional.l1_loss(model_output2, output).backward() + optimizer.step() + qweight = qembedding.weight.clone().to(torch.float32) + if below_zero: + qweight[qweight == 0] = -1 + nweight = quantization_function()(normal_embedding.weight) + assert torch.equal(model_output1, model_output2) + assert torch.equal(qweight, nweight) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +@pytest.mark.parametrize("optimizer", TEST_OPTIMIZERS) +def test_optimizer_is_cleared(vocab_size, embedding_size, quantization_function, optimizer): + model = BEmbedding( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + example_input = torch.tensor([0, 1, 2, 3, 1]) + optimizer = torch.optim.Adam(model.parameters(), lr=0.03) + before_size = len(optimizer.param_groups) + model.set_optimizer(optimizer) + model(example_input) + assert before_size + 1 == len(optimizer.param_groups) + model(example_input) + assert before_size + 1 == len(optimizer.param_groups) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +@pytest.mark.parametrize("optimizer", TEST_OPTIMIZERS) +def test_batched_bembedding_bag_training_duplicate(vocab_size, embedding_size, quantization_function, optimizer): + example_input = torch.tensor([1, 2, 3, 1, 1, 2]) + example_offsets = torch.tensor([0, 2, 3, 4, 5]) + example_output = torch.rand((len(example_offsets), 1)) + assert_equal_train_embedding_bag( + example_input, example_offsets, example_output, vocab_size, embedding_size, quantization_function, optimizer + ) + + +def assert_equal_train_embedding_bag( + input_indices, input_offsets, output, vocab_size, embedding_size, quantization_function, optimizer_class +): + input_indices.requires_grad_(False) + input_offsets.requires_grad_(False) + output.requires_grad_(False) + weights = (torch.rand((vocab_size, embedding_size)) * 100) - 50 + linear = torch.nn.Linear(embedding_size, 1).requires_grad_(False) + below_zero = False + if quantization_function()(weights).min().item() < 0: + below_zero = True + qembedding = BEmbeddingBag( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + sign_bool=below_zero, + ) + qembedding.set_weight(weights) + model = torch.nn.Sequential(qembedding, linear) + qoptimizer = optimizer_class(model.parameters(), lr=0.03) + qembedding.set_optimizer(qoptimizer) + + class NormalEmbeddingBag(torch.nn.Module): + def __init__(self, weight, q_function) -> None: + super().__init__() + self.weight = weight + self.q_function = q_function + + def forward(self, indices, offsets): + return embedding_bag(input=indices, offsets=offsets, weight=self.q_function(self.weight), sparse=False) + + normal_embedding = NormalEmbeddingBag(torch.clone(weights).requires_grad_(True), quantization_function()) + model = torch.nn.Sequential(normal_embedding, linear) + optimizer = optimizer_class(model.parameters(), lr=0.03) + + # Check if weights match before + qweight = qembedding.weight.clone().to(torch.float32) + if below_zero: + qweight[qweight == 0] = -1 + nweight = quantization_function()(normal_embedding.weight) + assert torch.equal(qweight, nweight) + + # First pass + model_output1_1 = linear(qembedding(input_indices, input_offsets)) + torch.nn.functional.l1_loss(model_output1_1, output).backward() + optimizer.step() + qembedding.step() + model_output2_1 = linear(normal_embedding(input_indices, input_offsets)) + torch.nn.functional.l1_loss(model_output2_1, output).backward() + optimizer.step() + + qweight = qembedding.weight.clone().to(torch.float32) + if below_zero: + qweight[qweight == 0] = -1 + nweight = quantization_function()(normal_embedding.weight) + assert torch.equal(qweight, nweight) + + # Second pass + model_output1_2 = linear(qembedding(input_indices, input_offsets)) + torch.nn.functional.l1_loss(model_output1_2, output).backward() + optimizer.step() + qembedding.step() + model_output2_2 = linear(normal_embedding(input_indices, input_offsets)) + torch.nn.functional.l1_loss(model_output2_2, output).backward() + optimizer.step() + + qweight = qembedding.weight.clone().to(torch.float32) + if below_zero: + qweight[qweight == 0] = -1 + nweight = quantization_function()(normal_embedding.weight) + assert torch.equal(model_output1_2, model_output2_2) + assert torch.equal(qweight, nweight) + + +@pytest.mark.parametrize("vocab_size, embedding_size", TEST_INPUT_DATA) +@pytest.mark.parametrize("quantization_function", TEST_QUANTIZATION_FUNCTIONS) +def test_bembedding_bag(vocab_size, embedding_size, quantization_function): + qembedding = BEmbeddingBag( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + example_input = torch.tensor([0, 1, 2]) + example_offsets = torch.tensor([0]) + + output = qembedding(example_input, example_offsets) + + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + + raw_embeddings = embedding_bag( + input=example_input, + offsets=example_offsets, + weight=binarized_embedding_table, + sparse=False, + ) + assert torch.equal(output, raw_embeddings) + + qembedding = BEmbeddingBag( + num_embeddings=vocab_size, + embedding_dim=embedding_size, + weight_quantization=quantization_function(), + ) + example_input = torch.tensor([0, 1, 2, 1, 2, 3]) + example_offsets = torch.tensor([0, 3]) + + output = qembedding(example_input, example_offsets) + + binarized_embedding_table = qembedding.weight.to(dtype=torch.float32) + + raw_embeddings = embedding_bag( + input=example_input, + offsets=example_offsets, + weight=binarized_embedding_table, + sparse=False, + ) + assert torch.equal(output, raw_embeddings) From 9ce624e1e5f0089ead2badd593e0eb36521c1fb7 Mon Sep 17 00:00:00 2001 From: Maximilian Schulze Date: Thu, 10 Nov 2022 16:40:40 +0100 Subject: [PATCH 172/208] fix formatting error --- examples/dlrm/facebook_dataloading/dataloading_fb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dlrm/facebook_dataloading/dataloading_fb.py b/examples/dlrm/facebook_dataloading/dataloading_fb.py index a3a2e6e..e4fea7a 100644 --- a/examples/dlrm/facebook_dataloading/dataloading_fb.py +++ b/examples/dlrm/facebook_dataloading/dataloading_fb.py @@ -70,7 +70,7 @@ def __init__( days = 24 out_file = "terabyte_processed" else: - raise(ValueError("Data set option is not supported")) + raise (ValueError("Data set option is not supported")) self.max_ind_range = max_ind_range self.memory_map = memory_map From 7ce377f4c405402c3f1b615da79dba95142459ab Mon Sep 17 00:00:00 2001 From: Snagnar Date: Wed, 16 Nov 2022 15:25:56 +0100 Subject: [PATCH 173/208] first draft of model zoo mechanics --- bitorch/models/base.py | 38 ++++++++++++++++++++++++++++++++++++++ bitorch/util.py | 13 ++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 7616d0d..c77afae 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,3 +1,4 @@ +import logging from argparse import ArgumentParser from typing import Optional, List, Any @@ -9,12 +10,14 @@ from bitorch.layers.qconv1d import QConv1dBase, QConv1d_NoAct from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct from bitorch.layers.qconv3d import QConv3dBase, QConv3d_NoAct +from bitorch.util import is_url class Model(nn.Module): """Base class for Bitorch models""" name = "" + pretrained_model_url = None def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() @@ -85,6 +88,41 @@ def initialize(self) -> None: def convert(self, new_mode: RuntimeMode, device: Optional[torch.device] = None, verbose: bool = False) -> "Model": return convert(self, new_mode, device, verbose) + def _generate_checkpoint_name(self): + # TODO: encode current runtime / layer implementation in name for better reference / correct loading of model + return f"{self.name}_checkpoint.pth" + + def from_pretrained(self, source: Optional[str] = None, verbose: bool = False) -> nn.Module: + if source is not None: + return self._load_from_source(source, verbose) + elif self.pretrained_model_url is not None: + return self._load_from_url(self.pretrained_model_url) + raise ValueError( + f"Model {self.name} does not have a predefined download url, " + "you have to specify a source to load the model from the specified checkpoint!" + ) + + def _load_from_url(self, url: str, verbose: bool = False): + ... + + def _load_from_source(self, source: str, verbose: bool = False): + ... + + def _upload_model(self, source_path: str, destination_url: str): + ... + + def store_checkpoint(self, destination: Optional[str] = None) -> None: + checkpoint_name = self._generate_checkpoint_name() + if is_url(destination): + logging.info(f"uploading model to url: {destination}...") + tmp_path = f"/tmp/{checkpoint_name}" + torch.save(self, tmp_path) + self._upload_model(tmp_path, destination) + else: + logging.debug(f"saving model to {destination}...") + torch.save(self, destination) + logging.debug("done saving model!") + class NoArgparseArgsMixin: """ diff --git a/bitorch/util.py b/bitorch/util.py index 92a2aa3..c2af955 100644 --- a/bitorch/util.py +++ b/bitorch/util.py @@ -1,4 +1,4 @@ -# import sys +import re import typing import importlib from typing import Optional, Callable, List, Any, Dict @@ -41,3 +41,14 @@ def filter_fn(x: Any) -> bool: lookup[transformed_key] = class_ return lookup + + +def is_url(input_str: str) -> bool: + regex = re.compile( + r'^(?:http|ftp)s?://' + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' + r'localhost|' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + r'(?::\d+)?' + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + return re.match(regex, input_str) is not None From 3856d21504bacf2494ca5eae7112716764ca0e2a Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 13 Oct 2022 14:51:03 +0200 Subject: [PATCH 174/208] add quicknet and tests --- bitorch/layers/__init__.py | 2 + bitorch/layers/pad.py | 33 ++++++++ bitorch/models/__init__.py | 8 ++ bitorch/models/common_layers.py | 42 ++++++++++ bitorch/models/quicknet.py | 135 ++++++++++++++++++++++++++++++++ tests/models/test_models.py | 6 ++ 6 files changed, 226 insertions(+) create mode 100644 bitorch/layers/pad.py create mode 100644 bitorch/models/quicknet.py diff --git a/bitorch/layers/__init__.py b/bitorch/layers/__init__.py index 26051ef..113a87b 100644 --- a/bitorch/layers/__init__.py +++ b/bitorch/layers/__init__.py @@ -19,6 +19,7 @@ from .qembedding import QEmbedding, QEmbeddingBag from .qlinear import QLinear, QLinearBase from .register import all_layer_registries +from .pad import PadModule __all__ = [ "InputGraphicalDebug", @@ -43,6 +44,7 @@ "Pact", "CustomImplementationMixin", "convert", + "PadModule", ] diff --git a/bitorch/layers/pad.py b/bitorch/layers/pad.py new file mode 100644 index 0000000..b24c0be --- /dev/null +++ b/bitorch/layers/pad.py @@ -0,0 +1,33 @@ +from torch import nn, Tensor +import torch.nn.functional as F + + +class PadModule(nn.Module): + """ + Module for padding tensors. + """ + + def __init__( + self, + padding_left: int = 0, + padding_right: int = 0, + padding_top: int = 0, + padding_bottom: int = 0, + padding_value: int = 0, + ): + """initialization function for padding. + + Args: + padding_left (int, optional): number of columns to pad to the left. + padding_right (int, optional): number of columns to pad to the right. + padding_top (int, optional): number of rows to pad at the top. + padding_bottom (int, optional): number of rows to pad at the bottom. + padding_value (float, optional): fill value used for padding. + """ + super(PadModule, self).__init__() + self.padding_tensor = (padding_left, padding_right, padding_top, padding_bottom) + self.padding_value = padding_value + + def forward(self, x: Tensor) -> Tensor: + x = F.pad(x, self.padding_tensor, "constant", self.padding_value) + return x diff --git a/bitorch/models/__init__.py b/bitorch/models/__init__.py index b43455b..87453ec 100644 --- a/bitorch/models/__init__.py +++ b/bitorch/models/__init__.py @@ -43,6 +43,11 @@ ResnetE18, ResnetE34, ) +from .quicknet import ( + QuickNet, + QuickNetSmall, + QuickNetLarge, +) from .dlrm import DLRM from ..util import build_lookup_dictionary @@ -79,6 +84,9 @@ "MeliusNetB", "MeliusNetC", "MeliusNetFlex", + "QuickNet", + "QuickNetSmall", + "QuickNetLarge", ] diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index f204121..4be12b3 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -1,6 +1,8 @@ from typing import List, Optional, Union from torch import nn +from bitorch.layers.pad import PadModule + def get_initial_layers( variant: Optional[Union[List[int], str]], input_channels: int, output_channels: int @@ -9,6 +11,46 @@ def get_initial_layers( layers: List[nn.Module] = [] if variant == (224, 224) or variant == "imagenet": layers.append(nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False)) + + elif variant == "quicknet_stem": + assert output_channels % 4 == 0 + stem_channels = output_channels // 4 + + layers.append(PadModule(0, 1, 0, 1)) + layers.append( + nn.Conv2d( + input_channels, + stem_channels, + kernel_size=3, + # padding="same", + stride=2, + bias=False, + ) + ) + layers.append(nn.BatchNorm2d(stem_channels, momentum=0.9)) + layers.append(nn.ReLU()) + layers.append(PadModule(0, 1, 0, 1)) + layers.append( + nn.Conv2d( + stem_channels, + stem_channels, + kernel_size=3, + groups=stem_channels, + # padding="same", + stride=2, + bias=False, + ) + ) + layers.append(nn.BatchNorm2d(stem_channels, momentum=0.9)) + layers.append( + nn.Conv2d( + stem_channels, + output_channels, + kernel_size=1, + bias=False, + ) + ) + layers.append(nn.BatchNorm2d(output_channels, momentum=0.9)) elif variant == "grouped_stem": stem_width = output_channels // 2 diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py new file mode 100644 index 0000000..6f3a651 --- /dev/null +++ b/bitorch/models/quicknet.py @@ -0,0 +1,135 @@ +import logging +from typing import Any, List + +import torch +from torch import nn +from torch.nn import Module + +from .base import Model, NoArgparseArgsMixin +from bitorch.layers import QConv2d, QLinear, PadModule +from bitorch.models.common_layers import get_initial_layers + + +class ResidualBlock(Module): + def __init__(self, in_channels: int, out_channels: int): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.body = self._build_body() + + def _build_body(self) -> nn.Sequential: + """builds body of residual blocks, i.e. a binary convolutions with a batchnorm. + + Returns: + nn.Sequential: the basic building block body model + """ + return nn.Sequential( + QConv2d( + self.in_channels, + self.out_channels, + kernel_size=3, + pad_value=1, + padding="same", + bias=False, + gradient_cancellation_threshold=1.25, + ), + nn.ReLU(), + nn.BatchNorm2d(self.out_channels, momentum=0.9), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.body(x) + x + + +def build_transition_block(in_channels: int, out_channels: int, strides: int) -> nn.Sequential: + return nn.Sequential( + nn.ReLU(), + nn.MaxPool2d(strides, stride=1), + PadModule(1, 1, 1, 1), + nn.Conv2d( + in_channels, + in_channels, + kernel_size=3, + groups=in_channels, + # padding="same", + stride=strides, + bias=False, + ).requires_grad_(False), + QConv2d(in_channels, out_channels, kernel_size=1, bias=False), + nn.ReLU(), + nn.BatchNorm2d(out_channels, momentum=0.9), + ) + + +class QuickNet(Model): + """QuickNet model from `"Larq Compute Engine: Design, Benchmark, and Deploy State-of-the-Art Binarized Neural Networks" + `_ paper. + """ + + name = "QuickNet" + + def __init__( + self, + input_shape: List[int], + section_filters: List[int] = [64, 128, 256, 512], + section_blocks: List[int] = [4, 4, 4, 4], + num_classes: int = 0, + ) -> None: + super(QuickNet, self).__init__(input_shape, num_classes) + self.image_channels = self._input_shape[1] + self.num_classes = num_classes + self.section_filters = section_filters + self.section_blocks = section_blocks + self._model = self._build_model() + logging.info("building Quicknet") + + def _build_model(self) -> nn.Sequential: + model = nn.Sequential() + model.add_module( + "Stem Module", + nn.Sequential(*get_initial_layers("quicknet_stem", self.image_channels, self.section_filters[0])), + ) + for block_num, (layers, filters) in enumerate(zip(self.section_blocks, self.section_filters)): + for layer in range(layers): + model.append(ResidualBlock(filters, filters)) + if block_num != len(self.section_blocks) - 1: + model.add_module( + "Transition_%d" % (block_num + 1), + build_transition_block(filters, self.section_filters[block_num + 1], 2), + ) + + model.add_module( + "Top", + nn.Sequential( + nn.ReLU(), + nn.AdaptiveAvgPool2d(1), + nn.Flatten(), + QLinear(self.section_filters[-1], self.num_classes), + nn.Softmax(), + ), + ) + return model + + +class QuickNetSmall(NoArgparseArgsMixin, QuickNet): + """QuickNetSmall model from `"Larq Compute Engine: Design, Benchmark, and Deploy State-of-the-Art Binarized Neural Networks" + `_ paper. + """ + + name = "QuickNetSmall" + section_filters = [32, 64, 256, 512] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(QuickNetSmall, self).__init__(section_filters=self.section_filters, *args, **kwargs) + + +class QuickNetLarge(NoArgparseArgsMixin, QuickNet): + """QuickNetLarge model from `"Larq Compute Engine: Design, Benchmark, and Deploy State-of-the-Art Binarized Neural Networks" + `_ paper. + """ + + name = "QuickNetLarge" + section_blocks = [6, 8, 12, 6] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super(QuickNetLarge, self).__init__(section_blocks=self.section_blocks, *args, **kwargs) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 34960ab..571d673 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -24,6 +24,9 @@ MeliusNetA, MeliusNetB, MeliusNetC, + QuickNet, + QuickNetSmall, + QuickNetLarge, ) import torch import numpy as np @@ -70,6 +73,9 @@ [ResnetE34, {}, RGB_DATASETS], [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], [DLRM, {}, [CRITEO]], + [QuickNet, {}, [IMAGENET]], + [QuickNetSmall, {}, [IMAGENET]], + [QuickNetLarge, {}, [IMAGENET]], ] From e9d04a930299325382b82aed82b8a1ae1b4d989b Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 20 Oct 2022 22:33:40 +0200 Subject: [PATCH 175/208] add initialization and naming for submodules quicknet --- bitorch/models/quicknet.py | 40 +++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index 6f3a651..ba4a148 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -6,7 +6,7 @@ from torch.nn import Module from .base import Model, NoArgparseArgsMixin -from bitorch.layers import QConv2d, QLinear, PadModule +from bitorch.layers import QConv2d, QLinear, PadModule, QConv2d_NoAct from bitorch.models.common_layers import get_initial_layers @@ -31,7 +31,6 @@ def _build_body(self) -> nn.Sequential: pad_value=1, padding="same", bias=False, - gradient_cancellation_threshold=1.25, ), nn.ReLU(), nn.BatchNorm2d(self.out_channels, momentum=0.9), @@ -55,7 +54,7 @@ def build_transition_block(in_channels: int, out_channels: int, strides: int) -> stride=strides, bias=False, ).requires_grad_(False), - QConv2d(in_channels, out_channels, kernel_size=1, bias=False), + nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False), nn.ReLU(), nn.BatchNorm2d(out_channels, momentum=0.9), ) @@ -83,21 +82,48 @@ def __init__( self._model = self._build_model() logging.info("building Quicknet") + self._model.Stem.apply(self._initialize_stem) # type: ignore + self._model.Body.apply(self._initialize_body_top) # type: ignore + self._model.Top.apply(self._initialize_body_top) # type: ignore + + def _initialize_stem(self, layer: Module) -> None: + if type(layer) == nn.Conv2d: + if layer.groups == 1: + nn.init.kaiming_normal_(layer.weight) # he normal + else: + nn.init.xavier_uniform_(layer.weight) # glorot uniform + + def _initialize_body_top(self, layer: Module) -> None: + if isinstance(layer, (QConv2d_NoAct, nn.Linear)): + if isinstance(layer, nn.Linear) or layer.groups == 1: + nn.init.xavier_normal_(layer.weight) # glorot normal + else: + pass # TODO add blurpool initialization + def _build_model(self) -> nn.Sequential: model = nn.Sequential() model.add_module( - "Stem Module", + "Stem", nn.Sequential(*get_initial_layers("quicknet_stem", self.image_channels, self.section_filters[0])), ) + body = nn.Sequential() for block_num, (layers, filters) in enumerate(zip(self.section_blocks, self.section_filters)): + residual_blocks: List[Module] = [] for layer in range(layers): - model.append(ResidualBlock(filters, filters)) + residual_blocks.append(ResidualBlock(filters, filters)) + body.add_module( + "ResidualBlocks_%d" % (block_num + 1), + nn.Sequential(*residual_blocks), + ) if block_num != len(self.section_blocks) - 1: - model.add_module( + body.add_module( "Transition_%d" % (block_num + 1), build_transition_block(filters, self.section_filters[block_num + 1], 2), ) - + model.add_module( + "Body", + body, + ) model.add_module( "Top", nn.Sequential( From 419919698461ee08253eaaf044cc711d05f8b0a9 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 20 Oct 2022 22:53:46 +0200 Subject: [PATCH 176/208] add weight clipping layer and specific quicknet function --- bitorch/layers/WeightClip.py | 23 +++++++++++++++++++++++ bitorch/models/quicknet.py | 12 +++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 bitorch/layers/WeightClip.py diff --git a/bitorch/layers/WeightClip.py b/bitorch/layers/WeightClip.py new file mode 100644 index 0000000..25768db --- /dev/null +++ b/bitorch/layers/WeightClip.py @@ -0,0 +1,23 @@ +from typing import Tuple, Any + +from torch.nn import Module + +from bitorch.layers import QConv2d_NoAct, QConv1d_NoAct, QConv3d_NoAct, QLinearBase, QEmbeddingBag, QEmbedding + + +class WeightClipper(object): + """ + Callable class for clipping weights of quantized or other layers. + """ + + qlayers = (QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, QLinearBase, QEmbeddingBag, QEmbedding) + + def __init__(self, clip_value: float = 1.0, layers: Tuple[Any, ...] = qlayers): + self.clip_value = clip_value + self.layers = layers + + def __call__(self, module: Module) -> None: + if isinstance(module, self.layers): + weights = module.weight.data + weights = weights.clamp(-self.clip_value, self.clip_value) # type: ignore + module.weight.data = weights diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index ba4a148..dec6294 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -87,7 +87,7 @@ def __init__( self._model.Top.apply(self._initialize_body_top) # type: ignore def _initialize_stem(self, layer: Module) -> None: - if type(layer) == nn.Conv2d: + if isinstance(layer, nn.Conv2d): if layer.groups == 1: nn.init.kaiming_normal_(layer.weight) # he normal else: @@ -159,3 +159,13 @@ class QuickNetLarge(NoArgparseArgsMixin, QuickNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(QuickNetLarge, self).__init__(section_blocks=self.section_blocks, *args, **kwargs) + + +def clip_weights(layer: Module, clip_value: float = 1.25) -> None: + """ + clips weights in quantized convolution layer in Residual Blocks. + """ + if isinstance(layer, ResidualBlock): + weights = layer.body._modules["0"].layer_implementation.weight.data # type: ignore + weights = weights.clamp(-clip_value, clip_value) # type: ignore + layer.body._modules["0"].layer_implementation.weight.data = weights # type: ignore From 32fd63a2cdec4bbd84fae080d7ced417cdd256a8 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Fri, 21 Oct 2022 19:00:03 +0200 Subject: [PATCH 177/208] add blurpool initialization --- bitorch/models/quicknet.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index dec6294..04e8aed 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -4,9 +4,10 @@ import torch from torch import nn from torch.nn import Module +import numpy as np from .base import Model, NoArgparseArgsMixin -from bitorch.layers import QConv2d, QLinear, PadModule, QConv2d_NoAct +from bitorch.layers import QConv2d, QLinear, PadModule from bitorch.models.common_layers import get_initial_layers @@ -86,6 +87,29 @@ def __init__( self._model.Body.apply(self._initialize_body_top) # type: ignore self._model.Top.apply(self._initialize_body_top) # type: ignore + def _blurpool_init(self, weight: torch.Tensor) -> None: + """Initialize anti-alias low_pass filter. + See the `"Making Convolutional Networks Shift-Invariant Again" `_ paper. + """ + + filters, kernel_size = weight.data.shape[0], weight.data.shape[2] + + if kernel_size == 2: + new_weights = np.array([1, 1]) + elif kernel_size == 3: + new_weights = np.array([1, 2, 1]) + elif kernel_size == 5: + new_weights = np.array([1, 4, 6, 4, 1]) + else: + raise ValueError("filter size should be in 2, 3, 5") + + new_weights = np.outer(new_weights, new_weights) + new_weights = new_weights / np.sum(new_weights) + new_weights = np.expand_dims(new_weights, axis=-1) + new_weights = np.repeat(new_weights, filters, axis=-1) + new_weights = np.reshape(new_weights, weight.shape) + weight.data = torch.from_numpy(new_weights) + def _initialize_stem(self, layer: Module) -> None: if isinstance(layer, nn.Conv2d): if layer.groups == 1: @@ -94,11 +118,11 @@ def _initialize_stem(self, layer: Module) -> None: nn.init.xavier_uniform_(layer.weight) # glorot uniform def _initialize_body_top(self, layer: Module) -> None: - if isinstance(layer, (QConv2d_NoAct, nn.Linear)): + if isinstance(layer, (nn.Conv2d, nn.Linear)): if isinstance(layer, nn.Linear) or layer.groups == 1: nn.init.xavier_normal_(layer.weight) # glorot normal else: - pass # TODO add blurpool initialization + self._blurpool_init(layer.weight) def _build_model(self) -> nn.Sequential: model = nn.Sequential() @@ -163,7 +187,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: def clip_weights(layer: Module, clip_value: float = 1.25) -> None: """ - clips weights in quantized convolution layer in Residual Blocks. + Clips weights in quantized convolution layer in Residual Blocks. + Can be used in training loop. """ if isinstance(layer, ResidualBlock): weights = layer.body._modules["0"].layer_implementation.weight.data # type: ignore From d85b5e4b9f6466a22c79b7a5bd60d8cd2a0184cb Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Fri, 21 Oct 2022 19:22:35 +0200 Subject: [PATCH 178/208] fix bug blurpool init --- bitorch/models/quicknet.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index 04e8aed..fc11b07 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -95,20 +95,18 @@ def _blurpool_init(self, weight: torch.Tensor) -> None: filters, kernel_size = weight.data.shape[0], weight.data.shape[2] if kernel_size == 2: - new_weights = np.array([1, 1]) + base = np.array([1.0, 1.0]) elif kernel_size == 3: - new_weights = np.array([1, 2, 1]) + base = np.array([1.0, 2.0, 1.0]) elif kernel_size == 5: - new_weights = np.array([1, 4, 6, 4, 1]) + base = np.array([1.0, 4.0, 6.0, 4.0, 1.0]) else: raise ValueError("filter size should be in 2, 3, 5") - new_weights = np.outer(new_weights, new_weights) - new_weights = new_weights / np.sum(new_weights) - new_weights = np.expand_dims(new_weights, axis=-1) - new_weights = np.repeat(new_weights, filters, axis=-1) - new_weights = np.reshape(new_weights, weight.shape) - weight.data = torch.from_numpy(new_weights) + new_weights = torch.Tensor(base[:, None] * base[None, :]) + new_weights = new_weights / torch.sum(new_weights) + new_weights = new_weights[None, None, :, :].repeat((filters, 1, 1, 1)) + weight.data = new_weights def _initialize_stem(self, layer: Module) -> None: if isinstance(layer, nn.Conv2d): From 3edd84186f854a916c9e0c68d9998fd4399c392b Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Sun, 23 Oct 2022 22:40:35 +0200 Subject: [PATCH 179/208] change last quicknet model layers and move clipping functions into class --- bitorch/models/quicknet.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index fc11b07..fbe7cfb 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -7,7 +7,7 @@ import numpy as np from .base import Model, NoArgparseArgsMixin -from bitorch.layers import QConv2d, QLinear, PadModule +from bitorch.layers import QConv2d, PadModule from bitorch.models.common_layers import get_initial_layers @@ -152,12 +152,21 @@ def _build_model(self) -> nn.Sequential: nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), - QLinear(self.section_filters[-1], self.num_classes), - nn.Softmax(), + nn.Linear(self.section_filters[-1], self.num_classes), ), ) return model + def _clip_weights(self, layer: Module, clip_value: float = 1.25) -> None: + """ + Clips weights in quantized convolution layer in Residual Blocks. + Can be used in training loop. + """ + if isinstance(layer, ResidualBlock): + weights = layer.body._modules["0"].layer_implementation.weight.data # type: ignore + weights = weights.clamp(-clip_value, clip_value) # type: ignore + layer.body._modules["0"].layer_implementation.weight.data = weights # type: ignore + class QuickNetSmall(NoArgparseArgsMixin, QuickNet): """QuickNetSmall model from `"Larq Compute Engine: Design, Benchmark, and Deploy State-of-the-Art Binarized Neural Networks" @@ -181,14 +190,3 @@ class QuickNetLarge(NoArgparseArgsMixin, QuickNet): def __init__(self, *args: Any, **kwargs: Any) -> None: super(QuickNetLarge, self).__init__(section_blocks=self.section_blocks, *args, **kwargs) - - -def clip_weights(layer: Module, clip_value: float = 1.25) -> None: - """ - Clips weights in quantized convolution layer in Residual Blocks. - Can be used in training loop. - """ - if isinstance(layer, ResidualBlock): - weights = layer.body._modules["0"].layer_implementation.weight.data # type: ignore - weights = weights.clamp(-clip_value, clip_value) # type: ignore - layer.body._modules["0"].layer_implementation.weight.data = weights # type: ignore From 4be9b6ffc0f667b9380c717602c1489b243f9436 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 10 Nov 2022 18:59:57 +0100 Subject: [PATCH 180/208] refactor quicknet --- bitorch/models/quicknet.py | 76 +++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index fbe7cfb..c68f45c 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -11,54 +11,47 @@ from bitorch.models.common_layers import get_initial_layers -class ResidualBlock(Module): - def __init__(self, in_channels: int, out_channels: int): +class ResidualBlock(nn.Sequential): + def __init__(self, in_channels: int, out_channels: int) -> None: super().__init__() - self.in_channels = in_channels - self.out_channels = out_channels - self.body = self._build_body() - - def _build_body(self) -> nn.Sequential: - """builds body of residual blocks, i.e. a binary convolutions with a batchnorm. - - Returns: - nn.Sequential: the basic building block body model - """ - return nn.Sequential( + self.add_module( + "qconv", QConv2d( - self.in_channels, - self.out_channels, + in_channels, + out_channels, kernel_size=3, pad_value=1, padding="same", bias=False, ), - nn.ReLU(), - nn.BatchNorm2d(self.out_channels, momentum=0.9), ) + self.add_module("relu", nn.ReLU()) + self.add_module("bnorm", nn.BatchNorm2d(out_channels, momentum=0.9)) def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.body(x) + x - - -def build_transition_block(in_channels: int, out_channels: int, strides: int) -> nn.Sequential: - return nn.Sequential( - nn.ReLU(), - nn.MaxPool2d(strides, stride=1), - PadModule(1, 1, 1, 1), - nn.Conv2d( - in_channels, - in_channels, - kernel_size=3, - groups=in_channels, - # padding="same", - stride=strides, - bias=False, - ).requires_grad_(False), - nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False), - nn.ReLU(), - nn.BatchNorm2d(out_channels, momentum=0.9), - ) + return super().forward(x) + x + + +class TransitionBlock(nn.Sequential): + def __init__(self, in_channels: int, out_channels: int, strides: int) -> None: + super().__init__() + self.add_module("relu", nn.ReLU()) + self.add_module("pool", nn.MaxPool2d(strides, stride=1)) + self.add_module("pad", PadModule(1, 1, 1, 1)) + self.add_module( + "depth_conv", + nn.Conv2d( + in_channels, + in_channels, + kernel_size=3, + groups=in_channels, + stride=strides, + bias=False, + ).requires_grad_(False), + ) + self.add_module("conv", nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)) + self.add_module("relu2", nn.ReLU()) + self.add_module("norm", nn.BatchNorm2d(out_channels, momentum=0.9)) class QuickNet(Model): @@ -139,8 +132,7 @@ def _build_model(self) -> nn.Sequential: ) if block_num != len(self.section_blocks) - 1: body.add_module( - "Transition_%d" % (block_num + 1), - build_transition_block(filters, self.section_filters[block_num + 1], 2), + "Transition_%d" % (block_num + 1), TransitionBlock(filters, self.section_filters[block_num + 1], 2) ) model.add_module( "Body", @@ -163,9 +155,9 @@ def _clip_weights(self, layer: Module, clip_value: float = 1.25) -> None: Can be used in training loop. """ if isinstance(layer, ResidualBlock): - weights = layer.body._modules["0"].layer_implementation.weight.data # type: ignore + weights = layer.qconv.weight.data # type: ignore weights = weights.clamp(-clip_value, clip_value) # type: ignore - layer.body._modules["0"].layer_implementation.weight.data = weights # type: ignore + layer.qconv.weight.data = weights # type: ignore class QuickNetSmall(NoArgparseArgsMixin, QuickNet): From c34e97448e7215b24d0ef9bce400d0293e60b87e Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 10 Nov 2022 21:17:34 +0100 Subject: [PATCH 181/208] add quicknet to changelog and minor changes --- CHANGELOG.md | 1 + bitorch/models/quicknet.py | 26 +++++++----- examples/image_classification/README.md | 4 +- tests/models/test_models.py | 54 ++++++++++++------------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b1a58..ac0c99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - new models: - [MeliusNet](bitorch/models/meliusnet.py) - [BinaryDenseNet](bitorch/models/densenet.py) + - [QuickNet](bitorch/models/quicknet.py) - simple example script for MNIST - support for integration of bitorch's inference engine for the following layers - QLinear diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index c68f45c..7684da2 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -64,11 +64,15 @@ class QuickNet(Model): def __init__( self, input_shape: List[int], - section_filters: List[int] = [64, 128, 256, 512], - section_blocks: List[int] = [4, 4, 4, 4], + section_filters: List[int] = None, + section_blocks: List[int] = None, num_classes: int = 0, ) -> None: super(QuickNet, self).__init__(input_shape, num_classes) + if section_filters is None: + section_filters = [64, 128, 256, 512] + if section_blocks is None: + section_blocks = [4, 4, 4, 4] self.image_channels = self._input_shape[1] self.num_classes = num_classes self.section_filters = section_filters @@ -76,9 +80,9 @@ def __init__( self._model = self._build_model() logging.info("building Quicknet") - self._model.Stem.apply(self._initialize_stem) # type: ignore - self._model.Body.apply(self._initialize_body_top) # type: ignore - self._model.Top.apply(self._initialize_body_top) # type: ignore + self._model.stem.apply(self._initialize_stem) # type: ignore + self._model.body.apply(self._initialize_body_top) # type: ignore + self._model.top.apply(self._initialize_body_top) # type: ignore def _blurpool_init(self, weight: torch.Tensor) -> None: """Initialize anti-alias low_pass filter. @@ -104,21 +108,21 @@ def _blurpool_init(self, weight: torch.Tensor) -> None: def _initialize_stem(self, layer: Module) -> None: if isinstance(layer, nn.Conv2d): if layer.groups == 1: - nn.init.kaiming_normal_(layer.weight) # he normal + nn.init.kaiming_normal_(layer.weight) else: - nn.init.xavier_uniform_(layer.weight) # glorot uniform + nn.init.xavier_uniform_(layer.weight) def _initialize_body_top(self, layer: Module) -> None: if isinstance(layer, (nn.Conv2d, nn.Linear)): if isinstance(layer, nn.Linear) or layer.groups == 1: - nn.init.xavier_normal_(layer.weight) # glorot normal + nn.init.xavier_normal_(layer.weight) else: self._blurpool_init(layer.weight) def _build_model(self) -> nn.Sequential: model = nn.Sequential() model.add_module( - "Stem", + "stem", nn.Sequential(*get_initial_layers("quicknet_stem", self.image_channels, self.section_filters[0])), ) body = nn.Sequential() @@ -135,11 +139,11 @@ def _build_model(self) -> nn.Sequential: "Transition_%d" % (block_num + 1), TransitionBlock(filters, self.section_filters[block_num + 1], 2) ) model.add_module( - "Body", + "body", body, ) model.add_module( - "Top", + "top", nn.Sequential( nn.ReLU(), nn.AdaptiveAvgPool2d(1), diff --git a/examples/image_classification/README.md b/examples/image_classification/README.md index 9b4d4cd..61a630a 100644 --- a/examples/image_classification/README.md +++ b/examples/image_classification/README.md @@ -42,14 +42,14 @@ The list below gives a brief overview over some selected arguments. ### model args -- `--model` specify name of model you want to train. Choose from `lenet,resnet,resnet152v1,resnet152v2,resnet18v1,resnet18v2,resnet34v1,resnet34v2,resnet50v1,resnet50v2,resnete,resnete18` or `resnete34` +- `--model` specify name of model you want to train. Choose from `Lenet,esnet,resnet152v1,resnet152v2,resnet18v1,resnet18v2,resnet34v1,resnet34v2,resnet50v1,resnet50v2,resnete,resnete18` or `resnete34` Each model can have specific arguments. Check them by calling `python image_classification.py --help`. ### dataset args - `--datset` name of dataset to train on. Chose from `mnist, cifar10, cifar100` and `imagenet` -- `--download` toggles if dataset if not present at `--dataset-dir` should be downloaded. Only available for `mnist` and `cifar10`. +- `--download` toggles if dataset is not present at `--dataset-dir` should be downloaded. Only available for `mnist` and `cifar10`. - `--dataset-dir` path to dataset. - `--num-worker` sets number of workers for dataloading diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 571d673..31ebfc3 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -45,33 +45,33 @@ RGB_DATASETS = [CIFAR10, CIFAR100, IMAGENET] TEST_INPUT_DATA = [ - [ - Resnet, - {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, - ALL_IMAGE_DATASETS, - ], - [Resnet18V1, {}, ALL_IMAGE_DATASETS], - [Resnet34V1, {}, ALL_IMAGE_DATASETS], - [Resnet50V1, {}, ALL_IMAGE_DATASETS], - [Resnet18V2, {}, ALL_IMAGE_DATASETS], - [Resnet34V2, {}, ALL_IMAGE_DATASETS], - [Resnet50V2, {}, ALL_IMAGE_DATASETS], - [DenseNet28, {}, ALL_IMAGE_DATASETS], - [DenseNet37, {}, ALL_IMAGE_DATASETS], - [DenseNet45, {}, ALL_IMAGE_DATASETS], - [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], - [MeliusNet22, {}, ALL_IMAGE_DATASETS], - [MeliusNet23, {}, ALL_IMAGE_DATASETS], - [MeliusNet42, {}, ALL_IMAGE_DATASETS], - [MeliusNet59, {}, ALL_IMAGE_DATASETS], - [MeliusNetA, {}, ALL_IMAGE_DATASETS], - [MeliusNetB, {}, ALL_IMAGE_DATASETS], - [MeliusNetC, {}, ALL_IMAGE_DATASETS], - [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], - [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], - [ResnetE18, {}, RGB_DATASETS], - [ResnetE34, {}, RGB_DATASETS], - [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], + # [ + # Resnet, + # {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, + # ALL_IMAGE_DATASETS, + # ], + # [Resnet18V1, {}, ALL_IMAGE_DATASETS], + # [Resnet34V1, {}, ALL_IMAGE_DATASETS], + # [Resnet50V1, {}, ALL_IMAGE_DATASETS], + # [Resnet18V2, {}, ALL_IMAGE_DATASETS], + # [Resnet34V2, {}, ALL_IMAGE_DATASETS], + # [Resnet50V2, {}, ALL_IMAGE_DATASETS], + # [DenseNet28, {}, ALL_IMAGE_DATASETS], + # [DenseNet37, {}, ALL_IMAGE_DATASETS], + # [DenseNet45, {}, ALL_IMAGE_DATASETS], + # [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], + # [MeliusNet22, {}, ALL_IMAGE_DATASETS], + # [MeliusNet23, {}, ALL_IMAGE_DATASETS], + # [MeliusNet42, {}, ALL_IMAGE_DATASETS], + # [MeliusNet59, {}, ALL_IMAGE_DATASETS], + # [MeliusNetA, {}, ALL_IMAGE_DATASETS], + # [MeliusNetB, {}, ALL_IMAGE_DATASETS], + # [MeliusNetC, {}, ALL_IMAGE_DATASETS], + # [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], + # [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], + # [ResnetE18, {}, RGB_DATASETS], + # [ResnetE34, {}, RGB_DATASETS], + # [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], [DLRM, {}, [CRITEO]], [QuickNet, {}, [IMAGENET]], [QuickNetSmall, {}, [IMAGENET]], From b5578145bf23c85932ed3e9e373a8d2c2a54a632 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 10 Nov 2022 21:18:29 +0100 Subject: [PATCH 182/208] remove weightclip class and add padding layer to changelog --- CHANGELOG.md | 1 + bitorch/layers/WeightClip.py | 23 ----------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 bitorch/layers/WeightClip.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0c99d..17efad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - callback to update Progressive Sign module - option to integrate custom models, datasets, quantization functions - a quantization scheduler which lets you change quantization methods during training +- a padding layer ### Changed diff --git a/bitorch/layers/WeightClip.py b/bitorch/layers/WeightClip.py deleted file mode 100644 index 25768db..0000000 --- a/bitorch/layers/WeightClip.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Tuple, Any - -from torch.nn import Module - -from bitorch.layers import QConv2d_NoAct, QConv1d_NoAct, QConv3d_NoAct, QLinearBase, QEmbeddingBag, QEmbedding - - -class WeightClipper(object): - """ - Callable class for clipping weights of quantized or other layers. - """ - - qlayers = (QConv1d_NoAct, QConv2d_NoAct, QConv3d_NoAct, QLinearBase, QEmbeddingBag, QEmbedding) - - def __init__(self, clip_value: float = 1.0, layers: Tuple[Any, ...] = qlayers): - self.clip_value = clip_value - self.layers = layers - - def __call__(self, module: Module) -> None: - if isinstance(module, self.layers): - weights = module.weight.data - weights = weights.clamp(-self.clip_value, self.clip_value) # type: ignore - module.weight.data = weights From 988234dcce911f409ad65d68b799ad9a717bd8d3 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Thu, 10 Nov 2022 21:39:43 +0100 Subject: [PATCH 183/208] reactivate tests and adapt README --- bitorch/models/common_layers.py | 2 - examples/image_classification/README.md | 2 +- tests/models/test_models.py | 54 ++++++++++++------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/bitorch/models/common_layers.py b/bitorch/models/common_layers.py index 4be12b3..2693b97 100644 --- a/bitorch/models/common_layers.py +++ b/bitorch/models/common_layers.py @@ -22,7 +22,6 @@ def get_initial_layers( input_channels, stem_channels, kernel_size=3, - # padding="same", stride=2, bias=False, ) @@ -36,7 +35,6 @@ def get_initial_layers( stem_channels, kernel_size=3, groups=stem_channels, - # padding="same", stride=2, bias=False, ) diff --git a/examples/image_classification/README.md b/examples/image_classification/README.md index 61a630a..7186a5c 100644 --- a/examples/image_classification/README.md +++ b/examples/image_classification/README.md @@ -42,7 +42,7 @@ The list below gives a brief overview over some selected arguments. ### model args -- `--model` specify name of model you want to train. Choose from `Lenet,esnet,resnet152v1,resnet152v2,resnet18v1,resnet18v2,resnet34v1,resnet34v2,resnet50v1,resnet50v2,resnete,resnete18` or `resnete34` +- `--model` specify name of model you want to train. Choose from `Lenet,Resnet,Resnet152V1,Resnet152V2,Resnet18V1,Resnet18V2,Resnet34V1,Resnet34V2,Resnet50V1,Resnet50V2,ResnetE,ResnetE18,ResnetE34,Quicknet,QuicknetSmall` or `QuickNetLarge` Each model can have specific arguments. Check them by calling `python image_classification.py --help`. diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 31ebfc3..571d673 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -45,33 +45,33 @@ RGB_DATASETS = [CIFAR10, CIFAR100, IMAGENET] TEST_INPUT_DATA = [ - # [ - # Resnet, - # {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, - # ALL_IMAGE_DATASETS, - # ], - # [Resnet18V1, {}, ALL_IMAGE_DATASETS], - # [Resnet34V1, {}, ALL_IMAGE_DATASETS], - # [Resnet50V1, {}, ALL_IMAGE_DATASETS], - # [Resnet18V2, {}, ALL_IMAGE_DATASETS], - # [Resnet34V2, {}, ALL_IMAGE_DATASETS], - # [Resnet50V2, {}, ALL_IMAGE_DATASETS], - # [DenseNet28, {}, ALL_IMAGE_DATASETS], - # [DenseNet37, {}, ALL_IMAGE_DATASETS], - # [DenseNet45, {}, ALL_IMAGE_DATASETS], - # [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], - # [MeliusNet22, {}, ALL_IMAGE_DATASETS], - # [MeliusNet23, {}, ALL_IMAGE_DATASETS], - # [MeliusNet42, {}, ALL_IMAGE_DATASETS], - # [MeliusNet59, {}, ALL_IMAGE_DATASETS], - # [MeliusNetA, {}, ALL_IMAGE_DATASETS], - # [MeliusNetB, {}, ALL_IMAGE_DATASETS], - # [MeliusNetC, {}, ALL_IMAGE_DATASETS], - # [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], - # [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], - # [ResnetE18, {}, RGB_DATASETS], - # [ResnetE34, {}, RGB_DATASETS], - # [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], + [ + Resnet, + {"resnet_version": [1, 2], "resnet_num_layers": [18, 34, 50]}, + ALL_IMAGE_DATASETS, + ], + [Resnet18V1, {}, ALL_IMAGE_DATASETS], + [Resnet34V1, {}, ALL_IMAGE_DATASETS], + [Resnet50V1, {}, ALL_IMAGE_DATASETS], + [Resnet18V2, {}, ALL_IMAGE_DATASETS], + [Resnet34V2, {}, ALL_IMAGE_DATASETS], + [Resnet50V2, {}, ALL_IMAGE_DATASETS], + [DenseNet28, {}, ALL_IMAGE_DATASETS], + [DenseNet37, {}, ALL_IMAGE_DATASETS], + [DenseNet45, {}, ALL_IMAGE_DATASETS], + [DenseNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], + [MeliusNet22, {}, ALL_IMAGE_DATASETS], + [MeliusNet23, {}, ALL_IMAGE_DATASETS], + [MeliusNet42, {}, ALL_IMAGE_DATASETS], + [MeliusNet59, {}, ALL_IMAGE_DATASETS], + [MeliusNetA, {}, ALL_IMAGE_DATASETS], + [MeliusNetB, {}, ALL_IMAGE_DATASETS], + [MeliusNetC, {}, ALL_IMAGE_DATASETS], + [MeliusNetFlex, {"flex_block_config": [[6, 6, 6, 5]]}, ALL_IMAGE_DATASETS], + [ResnetE, {"resnete_num_layers": [18, 34]}, RGB_DATASETS], + [ResnetE18, {}, RGB_DATASETS], + [ResnetE34, {}, RGB_DATASETS], + [LeNet, {"lenet_version": [0, 1, 2, 3, 4]}, [MNIST]], [DLRM, {}, [CRITEO]], [QuickNet, {}, [IMAGENET]], [QuickNetSmall, {}, [IMAGENET]], From 596dd4131fd94dd9556a31a3b16f868f17354578 Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Mon, 14 Nov 2022 17:41:10 +0100 Subject: [PATCH 184/208] adapt docstrings and type annotations --- bitorch/layers/pad.py | 4 +--- bitorch/models/quicknet.py | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bitorch/layers/pad.py b/bitorch/layers/pad.py index b24c0be..35c03fb 100644 --- a/bitorch/layers/pad.py +++ b/bitorch/layers/pad.py @@ -3,9 +3,7 @@ class PadModule(nn.Module): - """ - Module for padding tensors. - """ + """Module for padding tensors.""" def __init__( self, diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index 7684da2..87c6ca0 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -1,5 +1,5 @@ import logging -from typing import Any, List +from typing import Any, List, Optional import torch from torch import nn @@ -64,8 +64,8 @@ class QuickNet(Model): def __init__( self, input_shape: List[int], - section_filters: List[int] = None, - section_blocks: List[int] = None, + section_filters: Optional[List[int]] = None, + section_blocks: Optional[List[int]] = None, num_classes: int = 0, ) -> None: super(QuickNet, self).__init__(input_shape, num_classes) @@ -88,7 +88,6 @@ def _blurpool_init(self, weight: torch.Tensor) -> None: """Initialize anti-alias low_pass filter. See the `"Making Convolutional Networks Shift-Invariant Again" `_ paper. """ - filters, kernel_size = weight.data.shape[0], weight.data.shape[2] if kernel_size == 2: From 539a974984892d7ba2ccaa787ba01346ace33a4a Mon Sep 17 00:00:00 2001 From: Christopher Aust Date: Fri, 18 Nov 2022 14:07:19 +0100 Subject: [PATCH 185/208] add on_train_batch_end function to model base --- bitorch/models/base.py | 10 ++++++++++ bitorch/models/quicknet.py | 10 +++++----- .../image_classification.py | 19 ++++++++++++++++++- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 7616d0d..038e243 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -85,6 +85,16 @@ def initialize(self) -> None: def convert(self, new_mode: RuntimeMode, device: Optional[torch.device] = None, verbose: bool = False) -> "Model": return convert(self, new_mode, device, verbose) + def on_train_batch_end(self, layer: nn.Module) -> None: + """Is used with the pytorch lighting on_train_batch_end callback + + Implement it to e.g. clip weights after optimization. Is recursively applied to every submodule. + + Args: + layer (nn.Module): current layer + """ + pass + class NoArgparseArgsMixin: """ diff --git a/bitorch/models/quicknet.py b/bitorch/models/quicknet.py index 87c6ca0..600a8a3 100644 --- a/bitorch/models/quicknet.py +++ b/bitorch/models/quicknet.py @@ -152,16 +152,16 @@ def _build_model(self) -> nn.Sequential: ) return model - def _clip_weights(self, layer: Module, clip_value: float = 1.25) -> None: - """ - Clips weights in quantized convolution layer in Residual Blocks. - Can be used in training loop. - """ + def clip_weights(self, layer: Module, clip_value: float = 1.25) -> None: + """Clips weights in quantized convolution layer in Residual Blocks""" if isinstance(layer, ResidualBlock): weights = layer.qconv.weight.data # type: ignore weights = weights.clamp(-clip_value, clip_value) # type: ignore layer.qconv.weight.data = weights # type: ignore + def on_train_batch_end(self, layer: Module) -> None: + self.clip_weights(layer) + class QuickNetSmall(NoArgparseArgsMixin, QuickNet): """QuickNetSmall model from `"Larq Compute Engine: Design, Benchmark, and Deploy State-of-the-Art Binarized Neural Networks" diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index 8b75676..f7c74d2 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -18,9 +18,11 @@ import fvbitcore.nn as fv_nn import torch import wandb +import pytorch_lightning as pl from pytorch_lightning import Trainer -from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor +from pytorch_lightning.callbacks import ModelCheckpoint, LearningRateMonitor, Callback from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, LightningLoggerBase +from pytorch_lightning.utilities.types import STEP_OUTPUT from torch.utils.data import DataLoader import bitorch @@ -39,6 +41,19 @@ logger = logging.getLogger() +class ModelCallback(Callback): + def on_train_batch_end( + self, + trainer: Trainer, + pl_module: pl.LightningModule, + outputs: STEP_OUTPUT, + batch: Any, + batch_idx: int, + unused: int = 0, + ) -> None: + pl_module.model.apply(pl_module.model.on_train_batch_end) # type: ignore + + def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: """trains a model on the configured image dataset. @@ -85,6 +100,8 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) ) + callbacks.append(ModelCallback()) + # providing our own progress bar disables the default progress bar (not needed to disable later on) cmd_logger = CommandLineLogger(args.log_interval) callbacks.append(cmd_logger) From 4680e4c97ec0d8b700fb6402c07d64ac5347c387 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Sat, 19 Nov 2022 15:56:17 +0100 Subject: [PATCH 186/208] first draft of sync script --- bitorch/models/base.py | 2 +- .../image_classification.py | 9 ++- scripts/sync_wandb_nextcloud_hub.py | 76 +++++++++++++++++++ tests/layers/test_layer_implementation.py | 11 +-- 4 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 scripts/sync_wandb_nextcloud_hub.py diff --git a/bitorch/models/base.py b/bitorch/models/base.py index c77afae..8cee042 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -92,7 +92,7 @@ def _generate_checkpoint_name(self): # TODO: encode current runtime / layer implementation in name for better reference / correct loading of model return f"{self.name}_checkpoint.pth" - def from_pretrained(self, source: Optional[str] = None, verbose: bool = False) -> nn.Module: + def from_pretrained(self, *args, source: Optional[str] = None, mode: RuntimeMode = RuntimeMode.DEFAULT, verbose: bool = False, **kwargs) -> nn.Module: if source is not None: return self._load_from_source(source, verbose) elif self.pretrained_model_url is not None: diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index 8b75676..3445571 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -101,11 +101,14 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") + model_kwargs["input_shape"] = dataset.shape + model_kwargs["num_classes"] = dataset.num_classes - model = model_from_name(args.model)( - **model_kwargs, input_shape=dataset.shape, num_classes=dataset.num_classes - ) # type: ignore + model = model_from_name(args.model)(**model_kwargs) # type: ignore model.initialize() + model_kwargs["model_name"] = args.model + + wandb.config.update({"model_config": model_kwargs}) if args.quantization_scheduling: quantization_scheduler = Quantization_Scheduler( diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py new file mode 100644 index 0000000..3b029ff --- /dev/null +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -0,0 +1,76 @@ +from pathlib import Path +import json +import wandb +import logging +import argparse + +from examples.image_classification.datasets import dataset_from_name +from examples.image_classification.utils.arg_parser import create_argparser + +def main(args): + if args.config is not None: + config_path = Path(args.config) + if not config_path.exists(): + raise ValueError(f"config file {str(config_path.resolve())} does not exist!") + with config_path.open() as config_file: + config = json.parse(config_file) + else: + config = None + + print("args:", args, "wandb_entity:", args.wandb_entity) + api = wandb.Api() + filters = None if args.runs is None else {"$or": [{"config.experiment_name": run_name} for run_name in args.runs]} + entity = config["wandb_entity"] if config is not None else args.wandb_entity + project = config["wandb_project"] if config is not None else args.wandb_project + print("path:", f"{entity}/{project}") + runs = api.runs( + path=f"{entity}/{project}", + filters=filters, + ) + + for run in runs: + print("config:", run.config) + if "model_config" in run.config.keys(): + model_kwargs = run.config["model_config"] + else: + logging.info("DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command...") + + logging.debug("downloading run metadata...") + run.file("wandb-metadata.json").download("/tmp", replace=True) + + metadata_path = Path("/tmp/wandb-metadata.json") + if not metadata_path.exists(): + logging.error("metadata file could not be downloaded! skipping run...") + continue + + with metadata_path.open() as metadata_file: + metadata = json.parse(metadata_file) + + parser, model_parser = create_argparser(metadata["args"]) + + args_, unparsed_model_args = parser.parse_known_args() + model_args_ = model_parser.parse_args(unparsed_model_args) + + + dataset = dataset_from_name(args_.dataset) + + model_kwargs = vars(model_args_) + model_kwargs["input_shape"] = dataset.shape + model_kwargs["num_classes"] = dataset.num_classes + model_kwargs["model_name"] = args_.model + logging.debug(f"extracted model config: {model_kwargs}") + + model_name = model_kwargs["model_name"] + version_table = download_version_table(model_name) + + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("sync script of wandb runs and the model zoo") + parser.add_argument("--config", "-c", default=None, type=str, help="path to config json file") + parser.add_argument("--runs", "-r", nargs="*", default=None, help="the list of runs to sync. If omitted, all runs are synced.") + parser.add_argument("--wandb_entity", "-e", default=None, type=str) + parser.add_argument("--wandb_project", "-p", default=None, type=str) + args = parser.parse_args() + main(args) \ No newline at end of file diff --git a/tests/layers/test_layer_implementation.py b/tests/layers/test_layer_implementation.py index 09b9b1d..e4ec7f2 100644 --- a/tests/layers/test_layer_implementation.py +++ b/tests/layers/test_layer_implementation.py @@ -85,17 +85,18 @@ def test_recipe(): def test_default_impl(): + print("bitorch test mode:", bitorch.mode) layer = Example("Hello World", val=21) assert layer.val == 21 assert layer.class_name() == "BaseClass" assert isinstance(layer, Example.class_) assert isinstance(layer, LayerContainer) - + print(layer) # TODO: pickling is currently only possible in RAW mode - # content = pickle.dumps(layer) - # - # layer_loaded = pickle.loads(content) - # assert layer_loaded.val == 21 + content = pickle.dumps(layer) + + layer_loaded = pickle.loads(content) + assert layer_loaded.val == 21 def test_train_impl(): From 213291df19b87d23dc04e59c916cd43fb1c955e9 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Sat, 19 Nov 2022 18:54:40 +0100 Subject: [PATCH 187/208] some changes, might not work though.... --- bitorch/models/base.py | 2 +- scripts/sync_wandb_nextcloud_hub.py | 122 +++++++++++++++++++--------- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 8cee042..0062282 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -17,7 +17,7 @@ class Model(nn.Module): """Base class for Bitorch models""" name = "" - pretrained_model_url = None + hub_version_table_url = "" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py index 3b029ff..06ec777 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -1,12 +1,72 @@ +import pandas +import os from pathlib import Path import json import wandb import logging import argparse +from bitorch.models import model_from_name + from examples.image_classification.datasets import dataset_from_name from examples.image_classification.utils.arg_parser import create_argparser + +def extract_model_parameters(run): + print("config:", run.config) + if "model_config" in run.config.keys(): + model_kwargs = run.config["model_config"] + else: + logging.info( + "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command...") + + logging.debug("downloading run metadata...") + run.file("wandb-metadata.json").download("/tmp", replace=True) + + metadata_path = Path("/tmp/wandb-metadata.json") + if not metadata_path.exists(): + logging.error("metadata file could not be downloaded! skipping run...") + return + + with metadata_path.open() as metadata_file: + metadata = json.parse(metadata_file) + + parser, model_parser = create_argparser(metadata["args"]) + + args_, unparsed_model_args = parser.parse_known_args() + model_args_ = model_parser.parse_args(unparsed_model_args) + + dataset = dataset_from_name(args_.dataset) + + model_kwargs = vars(model_args_) + model_kwargs["input_shape"] = dataset.shape + model_kwargs["num_classes"] = dataset.num_classes + model_kwargs["model_name"] = args_.model + logging.debug(f"extracted model config: {model_kwargs}") + return model_kwargs + + +def download_version_table(model_name): + model_table_url = model_from_name(model_name).hub_version_table_url + model_table_url = "https://nextcloud.hpi.de/s/bAAMM9PwTBe95Qe/download/example_table.csv" + try: + os.system(f"curl -T /tmp/version_table.csv {model_table_url}") + version_table = pandas.read_csv("/tmp/version_table.csv") + except: + logging.info(f"could not retrieve model version table for {model_name}!") + return pandas.DataFrame() + + return version_table + + +def update_table(version_table, model_kwargs, run, compare_metrics): + ... + + +def write_table(version_table, model_name, table_base_url): + ... + + def main(args): if args.config is not None: config_path = Path(args.config) @@ -16,7 +76,7 @@ def main(args): config = json.parse(config_file) else: config = None - + print("args:", args, "wandb_entity:", args.wandb_entity) api = wandb.Api() filters = None if args.runs is None else {"$or": [{"config.experiment_name": run_name} for run_name in args.runs]} @@ -27,50 +87,36 @@ def main(args): path=f"{entity}/{project}", filters=filters, ) - + + metrics = config["metrics"] if config is not None else args.metrics + compare_metrics = {} + for metric in metrics: + metric_name, mode = metric.split("/") + if metric_name is None or mode is None: + logging.error( + f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max") + continue + compare_metrics[metric_name] = mode + + table_base_url = config["tables_url"] if config is not None else args.tables_url + for run in runs: - print("config:", run.config) - if "model_config" in run.config.keys(): - model_kwargs = run.config["model_config"] - else: - logging.info("DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command...") - - logging.debug("downloading run metadata...") - run.file("wandb-metadata.json").download("/tmp", replace=True) - - metadata_path = Path("/tmp/wandb-metadata.json") - if not metadata_path.exists(): - logging.error("metadata file could not be downloaded! skipping run...") - continue - - with metadata_path.open() as metadata_file: - metadata = json.parse(metadata_file) - - parser, model_parser = create_argparser(metadata["args"]) - - args_, unparsed_model_args = parser.parse_known_args() - model_args_ = model_parser.parse_args(unparsed_model_args) - - - dataset = dataset_from_name(args_.dataset) - - model_kwargs = vars(model_args_) - model_kwargs["input_shape"] = dataset.shape - model_kwargs["num_classes"] = dataset.num_classes - model_kwargs["model_name"] = args_.model - logging.debug(f"extracted model config: {model_kwargs}") - + + model_kwargs = extract_model_parameters(run) + model_name = model_kwargs["model_name"] version_table = download_version_table(model_name) - - - + version_table = update_table(version_table, model_kwargs, run, compare_metrics) + + write_table(version_table, model_name, table_base_url) + if __name__ == "__main__": parser = argparse.ArgumentParser("sync script of wandb runs and the model zoo") parser.add_argument("--config", "-c", default=None, type=str, help="path to config json file") - parser.add_argument("--runs", "-r", nargs="*", default=None, help="the list of runs to sync. If omitted, all runs are synced.") + parser.add_argument("--runs", "-r", nargs="*", default=None, + help="the list of runs to sync. If omitted, all runs are synced.") parser.add_argument("--wandb_entity", "-e", default=None, type=str) parser.add_argument("--wandb_project", "-p", default=None, type=str) args = parser.parse_args() - main(args) \ No newline at end of file + main(args) From 4eb31a1413ae9ce08ba3f078092c179ba4f71ece Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Tue, 29 Nov 2022 16:36:54 +0100 Subject: [PATCH 188/208] implemented wandb functionalities --- .../image_classification.py | 4 +- .../image_classification/utils/arg_parser.py | 4 +- scripts/config.json | 6 ++ scripts/sync_wandb_nextcloud_hub.py | 96 ++++++++++++++----- test.py | 42 ++++++++ 5 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 scripts/config.json create mode 100644 test.py diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index 3445571..533a46f 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -108,7 +108,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model.initialize() model_kwargs["model_name"] = args.model - wandb.config.update({"model_config": model_kwargs}) + if args.wandb_log: + # wandb.init() + wandb.config.update({"model_config": model_kwargs}) if args.quantization_scheduling: quantization_scheduler = Quantization_Scheduler( diff --git a/examples/image_classification/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py index 51c37aa..9942801 100644 --- a/examples/image_classification/utils/arg_parser.py +++ b/examples/image_classification/utils/arg_parser.py @@ -9,8 +9,8 @@ from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin from bitorch.quantizations.quantization_scheduler import Quantization_Scheduler -from datasets import dataset_names -from utils.teachers import available_teachers +from ..datasets import dataset_names +from ..utils.teachers import available_teachers class _HeadArgumentParser(ArgumentParser): diff --git a/scripts/config.json b/scripts/config.json new file mode 100644 index 0000000..66c98db --- /dev/null +++ b/scripts/config.json @@ -0,0 +1,6 @@ +{ + "entity": "hpi-deep-learning", + "project": "bitorch", + "compare_metrics": ["metrics/top-1-accuracy/max"], + "runs": null +} \ No newline at end of file diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py index 06ec777..6f41054 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -5,12 +5,30 @@ import wandb import logging import argparse +import datetime from bitorch.models import model_from_name from examples.image_classification.datasets import dataset_from_name from examples.image_classification.utils.arg_parser import create_argparser +INFINITY = 1e4 + +def add_missing_columns(*key_lists, init_values=None, table): + for list_idx, key_list in enumerate(key_lists): + for key_idx, key in enumerate(key_list): + if key not in table.columns: + value = ( + init_values + if not isinstance(init_values, list) + else ( + init_values[list_idx] + if isinstance(init_values[list_idx], list) + else init_values[list_idx][key_idx] + ) + ) + table[key] = value + return table def extract_model_parameters(run): print("config:", run.config) @@ -29,7 +47,9 @@ def extract_model_parameters(run): return with metadata_path.open() as metadata_file: - metadata = json.parse(metadata_file) + metadata = json.load(metadata_file) + + print parser, model_parser = create_argparser(metadata["args"]) @@ -46,25 +66,45 @@ def extract_model_parameters(run): return model_kwargs -def download_version_table(model_name): - model_table_url = model_from_name(model_name).hub_version_table_url - model_table_url = "https://nextcloud.hpi.de/s/bAAMM9PwTBe95Qe/download/example_table.csv" +def download_version_table(model_name, api): + model_table_path = model_from_name(model_name).version_table_path try: - os.system(f"curl -T /tmp/version_table.csv {model_table_url}") - version_table = pandas.read_csv("/tmp/version_table.csv") + # os.system(f"curl -T /tmp/version_table.csv {model_table_url}") + model_table = api.artifact(model_table_path).get_path(f"{model_name}.csv").download(root="/tmp") + version_table = pandas.read_csv(model_table) except: logging.info(f"could not retrieve model version table for {model_name}!") return pandas.DataFrame() return version_table +def upload_model_to_registry(run, api): + run.link_artifact() -def update_table(version_table, model_kwargs, run, compare_metrics): - ... + +def update_table(version_table, model_kwargs, run, compare_metrics, api): + for key in model_kwargs.keys(): + if key not in version_table.columns: + version_table[key] = None + + version_table = add_missing_columns( + model_kwargs.keys(), compare_metrics.keys(), ["time uploaded", "model_registry_version"], + init_values=[None, [-INFINITY if mode == "min" else +INFINITY for mode in compare_metrics.values()], ["", "latest"]], + table=version_table + ) + existing_row = version_table[(version_table == pandas.Series(model_kwargs)).all(1)] + if existing_row.empty: + model_kwargs.update(dict(run.summary)) + upload_model_to_registry(run, api) + version_table = pandas.concat([version_table, pandas.DataFrame([model_kwargs])]) + else: + ... -def write_table(version_table, model_name, table_base_url): - ... +def write_table(version_table, model_name, api): + version_table.to_csv(f"/tmp/{model_name}.csv") + model_table_path = model_from_name(model_name).version_table_path + api.artifact(model_table_path).add_file(f"/tmp/{model_name}.csv").save() def main(args): @@ -73,34 +113,38 @@ def main(args): if not config_path.exists(): raise ValueError(f"config file {str(config_path.resolve())} does not exist!") with config_path.open() as config_file: - config = json.parse(config_file) + config = json.load(config_file) else: - config = None + config = vars(args) - print("args:", args, "wandb_entity:", args.wandb_entity) + print("args:", args, "entity:", config["entity"]) api = wandb.Api() - filters = None if args.runs is None else {"$or": [{"config.experiment_name": run_name} for run_name in args.runs]} - entity = config["wandb_entity"] if config is not None else args.wandb_entity - project = config["wandb_project"] if config is not None else args.wandb_project + filters = None if config["runs"] is None else {"$or": [{"config.experiment_name": run_name} for run_name in config["runs"]]} + entity = config["entity"] + project = config["project"] print("path:", f"{entity}/{project}") runs = api.runs( path=f"{entity}/{project}", filters=filters, ) - metrics = config["metrics"] if config is not None else args.metrics + metrics = config["compare_metrics"] compare_metrics = {} for metric in metrics: - metric_name, mode = metric.split("/") - if metric_name is None or mode is None: + parts = metric.split("/") + metric_name = "/".join(parts[:-1]) + mode = parts[-1] + if metric_name is None or mode is None or mode not in ["min", "max"]: logging.error( f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max") continue compare_metrics[metric_name] = mode - - table_base_url = config["tables_url"] if config is not None else args.tables_url - - for run in runs: + + + for idx, run in enumerate(runs): + # for run in runs: + if idx < 15: + continue model_kwargs = extract_model_parameters(run) @@ -108,7 +152,7 @@ def main(args): version_table = download_version_table(model_name) version_table = update_table(version_table, model_kwargs, run, compare_metrics) - write_table(version_table, model_name, table_base_url) + write_table(version_table, model_name, api) if __name__ == "__main__": @@ -116,7 +160,7 @@ def main(args): parser.add_argument("--config", "-c", default=None, type=str, help="path to config json file") parser.add_argument("--runs", "-r", nargs="*", default=None, help="the list of runs to sync. If omitted, all runs are synced.") - parser.add_argument("--wandb_entity", "-e", default=None, type=str) - parser.add_argument("--wandb_project", "-p", default=None, type=str) + parser.add_argument("--entity", "-e", default=None, type=str) + parser.add_argument("--project", "-p", default=None, type=str) args = parser.parse_args() main(args) diff --git a/test.py b/test.py new file mode 100644 index 0000000..9e1022a --- /dev/null +++ b/test.py @@ -0,0 +1,42 @@ +import logging +import wandb + +def extract_artifact_version(artifact, model_name): + for alias in artifact._attrs["aliases"]: + if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): + return int(alias["alias"][1:]) + +def get_all_model_versions(entity, project, model_name): + api = wandb.Api(overrides={"entity": entity, "project": project}) + latest_model = api.artifact(f"{model_name}:latest", type="model") + latest_version = extract_artifact_version(latest_model, model_name) + + versions = [] + for i in range(latest_version + 1): + try: + versions.append(api.artifact(f"{model_name}:v{i}", type="model")) + except Exception as e: + print(f"{model_name} version v{i} could not be retrieved! skipping...") + pass + + print(f"retrieved {len(versions)} model versions!") + other_mdoel = wandb.Artifact(f"{entity}/{project}/{model_name}", type="model") + other_mdoel.add_file("model.ckpt") + other_mdoel.save() + return versions + +# run = wandb.init() +# api = wandb.Api(overrides={"entity": "snagnar", "project": "model-registry"}) +# artifact = run.use_artifact('snagnar/model-registry/bnn-model:v1', type='model') +# artifact_dir = artifact.download() +versions = get_all_model_versions("snagnar", "model-registry", "bnnmodel") + +for version in versions: + print(f"v{extract_artifact_version(version, 'bnnmodel')}") + +# print("now melius") +# versions = get_all_model_versions("hpi-deep-learning", "model-registry", "MeliusNet") + +# for version in versions: +# print(f"v{extract_artifact_version(version, 'MeliusNet')}") +... \ No newline at end of file From 6c67bd647d9a372777cc678fbffe20b92adc82f8 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Tue, 29 Nov 2022 18:10:48 +0100 Subject: [PATCH 189/208] some changes --- bitorch/models/base.py | 2 +- scripts/sync_wandb_nextcloud_hub.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 0062282..ad57973 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -17,7 +17,7 @@ class Model(nn.Module): """Base class for Bitorch models""" name = "" - hub_version_table_url = "" + version_table_path = "hpi-deep-learning/model-registry/model-version-tables" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py index 6f41054..09f2746 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -23,7 +23,7 @@ def add_missing_columns(*key_lists, init_values=None, table): if not isinstance(init_values, list) else ( init_values[list_idx] - if isinstance(init_values[list_idx], list) + if not isinstance(init_values[list_idx], list) else init_values[list_idx][key_idx] ) ) @@ -53,7 +53,7 @@ def extract_model_parameters(run): parser, model_parser = create_argparser(metadata["args"]) - args_, unparsed_model_args = parser.parse_known_args() + args_, unparsed_model_args = parser.parse_known_args(metadata["args"]) model_args_ = model_parser.parse_args(unparsed_model_args) dataset = dataset_from_name(args_.dataset) @@ -149,8 +149,8 @@ def main(args): model_kwargs = extract_model_parameters(run) model_name = model_kwargs["model_name"] - version_table = download_version_table(model_name) - version_table = update_table(version_table, model_kwargs, run, compare_metrics) + version_table = download_version_table(model_name, api) + version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) write_table(version_table, model_name, api) From 55588d31649326760c305c6f2aaf456df77b3575 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Wed, 30 Nov 2022 21:56:22 +0100 Subject: [PATCH 190/208] finished wandb registry syncing --- bitorch/models/base.py | 1 + examples/dlrm/utils/arg_parser.py | 2 +- .../image_classification/utils/arg_parser.py | 2 +- examples/image_classification/utils/utils.py | 4 +- scripts/config.json | 2 +- scripts/sync_wandb_nextcloud_hub.py | 169 +++++++++++++----- test.py | 100 +++++++---- 7 files changed, 202 insertions(+), 78 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index ad57973..53b10f6 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -18,6 +18,7 @@ class Model(nn.Module): name = "" version_table_path = "hpi-deep-learning/model-registry/model-version-tables" + model_registry_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/examples/dlrm/utils/arg_parser.py b/examples/dlrm/utils/arg_parser.py index c5d64f1..ff572d9 100644 --- a/examples/dlrm/utils/arg_parser.py +++ b/examples/dlrm/utils/arg_parser.py @@ -278,7 +278,7 @@ def add_regular_args(parser: ArgumentParser) -> None: ) -def create_argparser() -> Tuple[ArgumentParser, ArgumentParser]: +def create_argparser(arguments=None) -> Tuple[ArgumentParser, ArgumentParser]: """creates a main argument parser for general options and a model parser for model specific options Returns: diff --git a/examples/image_classification/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py index 9942801..2433aef 100644 --- a/examples/image_classification/utils/arg_parser.py +++ b/examples/image_classification/utils/arg_parser.py @@ -335,7 +335,7 @@ def add_regular_args(parser: ArgumentParser) -> None: parser.add_argument( "--model", type=str.lower, - choices=model_names(), + choices=model_names() + ["quicknet"], required=True, help="name of the model to be trained", ) diff --git a/examples/image_classification/utils/utils.py b/examples/image_classification/utils/utils.py index 2a55bbb..8ae811c 100644 --- a/examples/image_classification/utils/utils.py +++ b/examples/image_classification/utils/utils.py @@ -8,7 +8,7 @@ from torch.optim.optimizer import Optimizer -def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, output_stdout: bool) -> None: +def configure_logging(logger: Any, log_file: Optional[str], log_level: str, output_stdout: bool) -> None: """configures logging module. Args: @@ -27,7 +27,7 @@ def configure_logging(logger: Any, log_file: Union[None, str], log_level: str, o datefmt="%Y-%m-%d %H:%M:%S", ) - if log_file: + if log_file is not None: log_file_path = Path(log_file) log_file_path.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(log_file_path) diff --git a/scripts/config.json b/scripts/config.json index 66c98db..54db117 100644 --- a/scripts/config.json +++ b/scripts/config.json @@ -1,6 +1,6 @@ { "entity": "hpi-deep-learning", "project": "bitorch", - "compare_metrics": ["metrics/top-1-accuracy/max"], + "compare_metrics": ["metrics/test-top-1-accuracy/max"], "runs": null } \ No newline at end of file diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py index 09f2746..be23f47 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -1,3 +1,4 @@ +import warnings import pandas import os from pathlib import Path @@ -6,14 +7,51 @@ import logging import argparse import datetime +from typing import Any, Optional from bitorch.models import model_from_name +from bitorch.models.base import Model from examples.image_classification.datasets import dataset_from_name -from examples.image_classification.utils.arg_parser import create_argparser +# from examples.image_classification.utils.arg_parser import create_argparser +from importlib import import_module +warnings.filterwarnings("ignore") INFINITY = 1e4 +def configure_logging(logger: Any, log_file: Optional[str], log_level: str, output_stdout: bool) -> None: + """configures logging module. + + Args: + logger: the logger to be configured + log_file (str): path to log file. if omitted, logging will be forced to stdout. + log_level (str): string name of log level (e.g. 'debug') + output_stdout (bool): toggles stdout output. will be activated automatically if no log file was given. + otherwise if activated, logging will be outputed both to stdout and log file. + """ + log_level_name = log_level.upper() + log_level = getattr(logging, log_level_name) + logger.setLevel(log_level) + + logging_format = logging.Formatter( + "%(asctime)s - %(levelname)s [%(filename)s : %(funcName)s() : l. %(lineno)s]: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + if log_file is not None: + log_file_path = Path(log_file) + log_file_path.parent.mkdir(parents=True, exist_ok=True) + file_handler = logging.FileHandler(log_file_path) + file_handler.setFormatter(logging_format) + logger.addHandler(file_handler) + else: + output_stdout = True + + if output_stdout: + stream = logging.StreamHandler() + stream.setFormatter(logging_format) + logger.addHandler(stream) + def add_missing_columns(*key_lists, init_values=None, table): for list_idx, key_list in enumerate(key_lists): for key_idx, key in enumerate(key_list): @@ -31,12 +69,11 @@ def add_missing_columns(*key_lists, init_values=None, table): return table def extract_model_parameters(run): - print("config:", run.config) if "model_config" in run.config.keys(): model_kwargs = run.config["model_config"] else: logging.info( - "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command...") + "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command (experimental)...") logging.debug("downloading run metadata...") run.file("wandb-metadata.json").download("/tmp", replace=True) @@ -49,62 +86,109 @@ def extract_model_parameters(run): with metadata_path.open() as metadata_file: metadata = json.load(metadata_file) - print - + if "dlrm" in metadata["program"]: + create_argparser = import_module("examples.dlrm.utils.arg_parser").create_argparser + else: + create_argparser = import_module("examples.image_classification.utils.arg_parser").create_argparser + parser, model_parser = create_argparser(metadata["args"]) + parser.exit_on_error = False + model_parser.exit_on_error = False args_, unparsed_model_args = parser.parse_known_args(metadata["args"]) - model_args_ = model_parser.parse_args(unparsed_model_args) - - dataset = dataset_from_name(args_.dataset) + model_args_, _ = model_parser.parse_known_args(unparsed_model_args) model_kwargs = vars(model_args_) - model_kwargs["input_shape"] = dataset.shape - model_kwargs["num_classes"] = dataset.num_classes - model_kwargs["model_name"] = args_.model + if not "dlrm" in metadata["program"]: + dataset = dataset_from_name(args_.dataset) + model_kwargs["input_shape"] = dataset.shape + model_kwargs["num_classes"] = dataset.num_classes + model_kwargs["model_name"] = args_.model + else: + model_kwargs["model_name"] = "dlrm" logging.debug(f"extracted model config: {model_kwargs}") return model_kwargs +def extract_artifact_version(artifact, model_name): + for alias in artifact._attrs["aliases"]: + if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): + return int(alias["alias"][1:]) + -def download_version_table(model_name, api): - model_table_path = model_from_name(model_name).version_table_path +def download_version_table(api): + model_table_path = Model.version_table_path try: # os.system(f"curl -T /tmp/version_table.csv {model_table_url}") - model_table = api.artifact(model_table_path).get_path(f"{model_name}.csv").download(root="/tmp") + model_table = api.artifact(f"{model_table_path}:latest").get_path(f"versions.csv").download(root="/tmp") version_table = pandas.read_csv(model_table) - except: - logging.info(f"could not retrieve model version table for {model_name}!") + except Exception as e: + logging.info(f"could not retrieve model version table from {model_table_path}: {e}!") return pandas.DataFrame() return version_table -def upload_model_to_registry(run, api): - run.link_artifact() +def upload_model_to_registry(run, model_name, api): + model_registry_path = f"{model_from_name(model_name).model_registry_base_path}/{model_name}" + run_artifact = run.logged_artifacts()[0] + run_artifact.link(model_registry_path) + uploaded_artifact = api.artifact(f"{model_registry_path}:latest") + return extract_artifact_version(uploaded_artifact, model_name) +def compare(configA, configB, compare_metrics): + for metric_name, mode in compare_metrics: + if configA[metric_name] == configB[metric_name]: + continue + # this should be correct... + return (configA[metric_name] < configB[metric_name]) == (mode == "max") -def update_table(version_table, model_kwargs, run, compare_metrics, api): - for key in model_kwargs.keys(): - if key not in version_table.columns: - version_table[key] = None +def add_model_to_version_table(model_kwargs, version_table, run, api): + model_kwargs["model_registry_version"] = upload_model_to_registry(run, model_kwargs["model_name"], api) + return pandas.concat([version_table, pandas.DataFrame([model_kwargs])]) + +def update_table(version_table, model_kwargs, run, compare_metrics, api): version_table = add_missing_columns( - model_kwargs.keys(), compare_metrics.keys(), ["time uploaded", "model_registry_version"], - init_values=[None, [-INFINITY if mode == "min" else +INFINITY for mode in compare_metrics.values()], ["", "latest"]], + model_kwargs.keys(), dict(run.summary).keys(), [metric_name for metric_name, _ in compare_metrics], ["time uploaded", "model_registry_version"], + init_values=[None, None, [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], ["", "latest"]], table=version_table ) - existing_row = version_table[(version_table == pandas.Series(model_kwargs)).all(1)] + + print("comparing ", model_kwargs) + # this removes a deprecation warning + model_kwargs_series = pandas.Series(model_kwargs) + model_kwargs.update(dict(run.summary)) + # model_kwargs_series, version_table = model_kwargs_series.align(version_table, axis=0, copy=False) + + existing_row = version_table[(version_table == model_kwargs_series).all(1)] + model_kwargs["time uploaded"] = str(datetime.datetime.now()) + if existing_row.empty: - model_kwargs.update(dict(run.summary)) - upload_model_to_registry(run, api) - version_table = pandas.concat([version_table, pandas.DataFrame([model_kwargs])]) + logging.info("adding new model configuration to version table...") + version_table = add_model_to_version_table(model_kwargs, version_table, run, api) else: - ... + existing_model_kwargs = existing_row.to_dict() + existing_row_idx = (version_table == model_kwargs_series).all(1) + + # this prevents reuploading the same model + compare_metrics.append(["time uploaded", "min"]) + new_model_better = compare(existing_model_kwargs, model_kwargs, compare_metrics) + if new_model_better: + logging.info("overwriting preexisting model configuration in version table with better version...") + version_table = version_table.drop(existing_row_idx) + version_table = add_model_to_version_table(model_kwargs, version_table, run, api) + else: + logging.info("better/older model with same config already exists in version table, skipping...") + return version_table def write_table(version_table, model_name, api): - version_table.to_csv(f"/tmp/{model_name}.csv") - model_table_path = model_from_name(model_name).version_table_path - api.artifact(model_table_path).add_file(f"/tmp/{model_name}.csv").save() + version_table.to_csv(f"/tmp/versions.csv") + entity, project, _ = model_from_name(model_name).version_table_path.split("/") + with wandb.init(entity=entity, project=project) as run: + version_table_artifact = wandb.Artifact("model-tables", type="tables") + version_table_artifact.add_file("/tmp/versions.csv") + run.log_artifact(version_table_artifact) + run.link_artifact(version_table_artifact, model_from_name(model_name).version_table_path) def main(args): @@ -116,6 +200,8 @@ def main(args): config = json.load(config_file) else: config = vars(args) + + configure_logging(logging.getLogger(), None, "info", True) print("args:", args, "entity:", config["entity"]) api = wandb.Api() @@ -129,7 +215,7 @@ def main(args): ) metrics = config["compare_metrics"] - compare_metrics = {} + compare_metrics = [] for metric in metrics: parts = metric.split("/") metric_name = "/".join(parts[:-1]) @@ -138,21 +224,24 @@ def main(args): logging.error( f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max") continue - compare_metrics[metric_name] = mode - + compare_metrics.append([metric_name, mode]) + version_table = download_version_table(api) for idx, run in enumerate(runs): # for run in runs: - if idx < 15: + if idx < 2: + continue + # try: + if len(run.logged_artifacts()) == 0: + logging.info(f"run {run.name} has no logged artifacts, skipping...") continue - model_kwargs = extract_model_parameters(run) - model_name = model_kwargs["model_name"] - version_table = download_version_table(model_name, api) version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) - write_table(version_table, model_name, api) + write_table(version_table, model_name, api) + # except Exception as e: + # logging.error(f"could not sync run {run.name}:{e}. Skipping...") if __name__ == "__main__": diff --git a/test.py b/test.py index 9e1022a..245cad4 100644 --- a/test.py +++ b/test.py @@ -1,42 +1,76 @@ import logging import wandb -def extract_artifact_version(artifact, model_name): - for alias in artifact._attrs["aliases"]: - if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): - return int(alias["alias"][1:]) - -def get_all_model_versions(entity, project, model_name): - api = wandb.Api(overrides={"entity": entity, "project": project}) - latest_model = api.artifact(f"{model_name}:latest", type="model") - latest_version = extract_artifact_version(latest_model, model_name) +# def extract_artifact_version(artifact, model_name): +# for alias in artifact._attrs["aliases"]: +# if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): +# return int(alias["alias"][1:]) + +# def get_all_model_versions(entity, project, model_name): +# api = wandb.Api(overrides={"entity": entity, "project": project}) +# latest_model = api.artifact(f"{model_name}:latest", type="model") +# latest_version = extract_artifact_version(latest_model, model_name) - versions = [] - for i in range(latest_version + 1): - try: - versions.append(api.artifact(f"{model_name}:v{i}", type="model")) - except Exception as e: - print(f"{model_name} version v{i} could not be retrieved! skipping...") - pass +# versions = [] +# for i in range(latest_version + 1): +# try: +# versions.append(api.artifact(f"{model_name}:v{i}", type="model")) +# except Exception as e: +# print(f"{model_name} version v{i} could not be retrieved! skipping...") +# pass - print(f"retrieved {len(versions)} model versions!") - other_mdoel = wandb.Artifact(f"{entity}/{project}/{model_name}", type="model") - other_mdoel.add_file("model.ckpt") - other_mdoel.save() - return versions +# print(f"retrieved {len(versions)} model versions!") +# other_mdoel = wandb.Artifact(f"{entity}/{project}/{model_name}", type="model") +# other_mdoel.add_file("model.ckpt") +# other_mdoel.save() +# return versions -# run = wandb.init() -# api = wandb.Api(overrides={"entity": "snagnar", "project": "model-registry"}) -# artifact = run.use_artifact('snagnar/model-registry/bnn-model:v1', type='model') -# artifact_dir = artifact.download() -versions = get_all_model_versions("snagnar", "model-registry", "bnnmodel") +# # run = wandb.init() +# # api = wandb.Api(overrides={"entity": "snagnar", "project": "model-registry"}) +# # artifact = run.use_artifact('snagnar/model-registry/bnn-model:v1', type='model') +# # artifact_dir = artifact.download() +# versions = get_all_model_versions("snagnar", "model-registry", "bnnmodel") -for version in versions: - print(f"v{extract_artifact_version(version, 'bnnmodel')}") +# for version in versions: +# print(f"v{extract_artifact_version(version, 'bnnmodel')}") -# print("now melius") -# versions = get_all_model_versions("hpi-deep-learning", "model-registry", "MeliusNet") +# # print("now melius") +# # versions = get_all_model_versions("hpi-deep-learning", "model-registry", "MeliusNet") -# for version in versions: -# print(f"v{extract_artifact_version(version, 'MeliusNet')}") -... \ No newline at end of file +# # for version in versions: +# # print(f"v{extract_artifact_version(version, 'MeliusNet')}") +# ... + +from pathlib import Path +import random +# # with wandb.init(entity="hpi-deep-learning", project="model-registry") as run: +with wandb.init(entity="hpi-deep-learning", project="model-registry") as run: + table_art = wandb.Artifact("model-tables", type="tables") + # with Path("versions.csv").open("w") as v: + # v.write(f"{random.randint(0, 1e6)}") + + # with Path("versions.csv").open() as v: + # print("v:", v.readlines()) + table_art.add_file("versions.csv") + # table_art.save() + # table_art.wait() + run.log_artifact(table_art) + # synced_table_art = run.logged_artifacts()[0] + run.link_artifact(table_art, "hpi-deep-learning/model-registry/model-version-tables") + +# table_art.link("hpi-deep-learning/model-registry/model-version-tables") + +# import wandb +# Initialize a W&B run to start tracking +# wandb.init() + +# # Create an Model Version +# art = wandb.Artifact("model-tables", type="tables") + +# art.add_file("versions.csv") + +# # Log the Model Version +# wandb.log_artifact(art) + +# # Link the Model Version to the Collection +# wandb.run.link_artifact(art, "model-registry/bnnmodel") \ No newline at end of file From 840beb063bbd32bda4003d2996c6ca84026cb24f Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Thu, 1 Dec 2022 13:16:09 +0100 Subject: [PATCH 191/208] some more progress with syncin --- scripts/config.json | 2 +- scripts/sync_wandb_nextcloud_hub.py | 56 ++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/scripts/config.json b/scripts/config.json index 54db117..b9acd27 100644 --- a/scripts/config.json +++ b/scripts/config.json @@ -1,6 +1,6 @@ { "entity": "hpi-deep-learning", "project": "bitorch", - "compare_metrics": ["metrics/test-top-1-accuracy/max"], + "compare_metrics": ["metrics/best-test-top1-accuracy/max"], "runs": null } \ No newline at end of file diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_wandb_nextcloud_hub.py index be23f47..70c35c2 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_wandb_nextcloud_hub.py @@ -1,3 +1,6 @@ +import sys +from tqdm import tqdm +import numpy as np import warnings import pandas import os @@ -141,12 +144,22 @@ def compare(configA, configB, compare_metrics): # this should be correct... return (configA[metric_name] < configB[metric_name]) == (mode == "max") +def convert_dtypes(data): + for column in data.columns: + if data[column].dtype.kind not in 'biufc': + data[column] = data[column].astype(str) + return data + def add_model_to_version_table(model_kwargs, version_table, run, api): model_kwargs["model_registry_version"] = upload_model_to_registry(run, model_kwargs["model_name"], api) - return pandas.concat([version_table, pandas.DataFrame([model_kwargs])]) + df_to_add = pandas.DataFrame([model_kwargs]) + # df_to_add = pandas.DataFrame([model_kwargs], dtype="string").apply(pandas.to_numeric, errors="ignore") + return pandas.concat([version_table, df_to_add], ignore_index=True) def update_table(version_table, model_kwargs, run, compare_metrics, api): + model_comparison_keys = list(model_kwargs.keys()) + model_kwargs = convert_dtypes(model_kwargs) version_table = add_missing_columns( model_kwargs.keys(), dict(run.summary).keys(), [metric_name for metric_name, _ in compare_metrics], ["time uploaded", "model_registry_version"], init_values=[None, None, [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], ["", "latest"]], @@ -155,26 +168,28 @@ def update_table(version_table, model_kwargs, run, compare_metrics, api): print("comparing ", model_kwargs) # this removes a deprecation warning + # model_kwargs_series = pandas.Series(model_kwargs, dtype="string").apply(pandas.to_numeric, errors="ignore") model_kwargs_series = pandas.Series(model_kwargs) - model_kwargs.update(dict(run.summary)) + existing_row = version_table[(version_table[model_comparison_keys] == model_kwargs_series).all(1)] # model_kwargs_series, version_table = model_kwargs_series.align(version_table, axis=0, copy=False) - existing_row = version_table[(version_table == model_kwargs_series).all(1)] + + model_kwargs.update(dict(run.summary)) model_kwargs["time uploaded"] = str(datetime.datetime.now()) if existing_row.empty: logging.info("adding new model configuration to version table...") version_table = add_model_to_version_table(model_kwargs, version_table, run, api) else: - existing_model_kwargs = existing_row.to_dict() - existing_row_idx = (version_table == model_kwargs_series).all(1) + existing_row_idx = np.where((version_table[model_comparison_keys] == model_kwargs_series).all(1))[0][0] + existing_model_kwargs = version_table.iloc[existing_row_idx].to_dict() # this prevents reuploading the same model compare_metrics.append(["time uploaded", "min"]) new_model_better = compare(existing_model_kwargs, model_kwargs, compare_metrics) if new_model_better: logging.info("overwriting preexisting model configuration in version table with better version...") - version_table = version_table.drop(existing_row_idx) + version_table = version_table.drop(existing_row_idx).reset_index(drop=True) version_table = add_model_to_version_table(model_kwargs, version_table, run, api) else: logging.info("better/older model with same config already exists in version table, skipping...") @@ -227,10 +242,7 @@ def main(args): compare_metrics.append([metric_name, mode]) version_table = download_version_table(api) - for idx, run in enumerate(runs): - # for run in runs: - if idx < 2: - continue + for run in tqdm(runs): # try: if len(run.logged_artifacts()) == 0: logging.info(f"run {run.name} has no logged artifacts, skipping...") @@ -238,10 +250,24 @@ def main(args): model_kwargs = extract_model_parameters(run) model_name = model_kwargs["model_name"] version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) + # except Exception as e: + # logging.info(f"run {run.name} cannot be synced with registry due to error: {e}. skipping...") write_table(version_table, model_name, api) - # except Exception as e: - # logging.error(f"could not sync run {run.name}:{e}. Skipping...") + logging.info(f"Successfully synced {len(runs)} runs!") + +def delete_model_version_table_in_registry(): + logging.warn("DELETING MODEL VERSION TABLE IN REGISTRY...") + input("PRESS ENTER IF YOU ARE SURE YOU WANT TO CONTINUE:") + entity, project, _ = Model.version_table_path.split("/") + with wandb.init(entity=entity, project=project) as run: + table_art = wandb.Artifact("model-tables", type="tables") + with Path("versions.csv").open("w") as v: + v.write("") + table_art.add_file("versions.csv") + run.log_artifact(table_art) + run.link_artifact(table_art, Model.version_table_path) + logging.warn("MODEL VERSION TABLE DELETED!") if __name__ == "__main__": @@ -251,5 +277,11 @@ def main(args): help="the list of runs to sync. If omitted, all runs are synced.") parser.add_argument("--entity", "-e", default=None, type=str) parser.add_argument("--project", "-p", default=None, type=str) + parser.add_argument("--delete-version-table", default=False, action="store_true", + help="deletes the remote version table on the model registry. Use with caution!") args = parser.parse_args() + + if args.delete_version_table: + delete_model_version_table_in_registry() + sys.exit(0) main(args) From 2bf24245a8105c712f9f365b8ee5ecf8afb093ce Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 2 Dec 2022 14:45:50 +0100 Subject: [PATCH 192/208] added model weight download from wandb hub --- bitorch/models/base.py | 36 ++--- bitorch/models/model_hub.py | 63 +++++++++ examples/dlrm/train_dlrm.py | 13 +- .../image_classification.py | 21 +-- .../image_classification/requirements.txt | 2 +- .../image_classification/utils/arg_parser.py | 11 +- .../image_classification/utils/callbacks.py | 2 +- .../utils/lightning_model.py | 2 +- scripts/config.json | 5 +- ...ndb_nextcloud_hub.py => sync_model_hub.py} | 128 ++++++------------ 10 files changed, 161 insertions(+), 122 deletions(-) create mode 100644 bitorch/models/model_hub.py rename scripts/{sync_wandb_nextcloud_hub.py => sync_model_hub.py} (67%) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 53b10f6..7eac74b 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,3 +1,4 @@ +import pandas import logging from argparse import ArgumentParser from typing import Optional, List, Any @@ -11,6 +12,8 @@ from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct from bitorch.layers.qconv3d import QConv3dBase, QConv3d_NoAct from bitorch.util import is_url +from bitorch.models.model_hub import load_from_hub +import wandb class Model(nn.Module): @@ -18,7 +21,7 @@ class Model(nn.Module): name = "" version_table_path = "hpi-deep-learning/model-registry/model-version-tables" - model_registry_base_path = "hpi-deep-learning/model-registry" + model_hub_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() @@ -93,24 +96,21 @@ def _generate_checkpoint_name(self): # TODO: encode current runtime / layer implementation in name for better reference / correct loading of model return f"{self.name}_checkpoint.pth" - def from_pretrained(self, *args, source: Optional[str] = None, mode: RuntimeMode = RuntimeMode.DEFAULT, verbose: bool = False, **kwargs) -> nn.Module: + @classmethod + def from_pretrained(cls, source: Optional[str] = None, mode: RuntimeMode = RuntimeMode.DEFAULT, **kwargs) -> nn.Module: + model = cls(**kwargs) if source is not None: - return self._load_from_source(source, verbose) - elif self.pretrained_model_url is not None: - return self._load_from_url(self.pretrained_model_url) - raise ValueError( - f"Model {self.name} does not have a predefined download url, " - "you have to specify a source to load the model from the specified checkpoint!" - ) - - def _load_from_url(self, url: str, verbose: bool = False): - ... - - def _load_from_source(self, source: str, verbose: bool = False): - ... - - def _upload_model(self, source_path: str, destination_url: str): - ... + logging.info(f"Loading {cls.name} model state_dict from file {source}") + state_dict = torch.load(source) + else: + kwargs["model_name"] = cls.name.lower() + logging.info(f"Downloading {cls.name} model state_dict from hub...") + state_dict = load_from_hub(cls.version_table_path, cls.model_hub_base_path, **kwargs) + + model.load_state_dict(state_dict) + return model + + def store_checkpoint(self, destination: Optional[str] = None) -> None: checkpoint_name = self._generate_checkpoint_name() diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py new file mode 100644 index 0000000..6298824 --- /dev/null +++ b/bitorch/models/model_hub.py @@ -0,0 +1,63 @@ +import wandb +import numbers +import pandas +import logging +import warnings +import torch + +def convert_dtypes(data): + for key, value in data.items(): + if isinstance(value, list): + value = tuple(value) + if not isinstance(value, numbers.Number) and not isinstance(value, bool): + data[key] = str(value) + return data + +def get_matching_row(version_table, model_kwargs): + model_kwargs = convert_dtypes(model_kwargs) + with warnings.catch_warnings(): + model_kwargs_series = pandas.Series(model_kwargs) + existing_row = version_table[(version_table[model_kwargs.keys()] == model_kwargs_series).all(1)] + if existing_row.empty: + return None + return existing_row + +def get_model_path(version_table, model_kwargs, model_hub_base_path): + matching_row = get_matching_row(version_table, model_kwargs) + if matching_row is None: + raise RuntimeError(f"No matching model found in hub with configuration: {model_kwargs}! You can train" + " it yourself or try to load it from a local checkpoint!") + model_version = matching_row["model_hub_version"][0] + return f"{model_hub_base_path}/{model_kwargs['model_name']}:v{model_version}" + +def load_from_hub(model_version_table_path, model_hub_base_path, download_path="/tmp", **model_kwargs): + api = wandb.Api() + version_table = download_version_table(model_version_table_path, api) + model_path = get_model_path(version_table, model_kwargs, model_hub_base_path) + logging.info("downloading model...") + downloaded_model = api.artifact(model_path).get_path("model.ckpt").download(root=download_path) + artifact = torch.load(downloaded_model) + + # true if artifact is a checkpoint from pytorch lightning + if isinstance(artifact, dict): + return lightning_checkpoint_to_state_dict(artifact) + return artifact + +def lightning_checkpoint_to_state_dict(artifact): + state_dict = artifact["state_dict"] + # turns model._model.arg keys in state dict into _model.arg + extracted_state_dict = {key[6:]: value for key, value in state_dict.items()} + return extracted_state_dict + +def download_version_table(model_table_path, api, no_exception=False): + logging.info("downloading model version table from hub...") + try: + model_table = api.artifact(f"{model_table_path}:latest").get_path(f"versions.csv").download(root="/tmp") + version_table = pandas.read_csv(model_table) + except Exception as e: + logging.info(f"could not retrieve model version table from {model_table_path}: {e}") + if no_exception: + logging.info("creating empty table...") + return pandas.DataFrame() + raise Exception(e) + return version_table \ No newline at end of file diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index bbf4c9b..7c7b138 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -151,14 +151,25 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: print("DATA SHAPE:", (type(data_point[0]), (type(data_point[1]), type(data_point[2])))) print("DATA SHAPE:", (data_point[0].shape, (data_point[1].shape, data_point[2].shape))) data_point = (data_point[0], (data_point[1], data_point[2])) + + # for model registry compliance + model_kwargs["embedding_layer_sizes"] = embedding_layer_sizes + model_kwargs["input_shape"] = [] + model_kwargs["dense_feature_size"] = dense_feature_size - model = DLRM(**model_kwargs, embedding_layer_sizes=embedding_layer_sizes, input_shape=[], dense_feature_size=dense_feature_size) # type: ignore + model = DLRM(**model_kwargs) # type: ignore model.initialize() if args.checkpoint_load is not None and args.pretrained: logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = ModelWrapper(model, 1, args) + + + # for model registry compliance + model_kwargs["model_name"] = args.model + if args.wandb_log: + wandb.config.update({"model_config": model_kwargs}) trainer = Trainer( strategy=args.strategy, diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index 533a46f..eb9a940 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -101,15 +101,18 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - model_kwargs["input_shape"] = dataset.shape + + model_kwargs["input_shape"] = tuple(dataset.shape) model_kwargs["num_classes"] = dataset.num_classes - - model = model_from_name(args.model)(**model_kwargs) # type: ignore - model.initialize() - model_kwargs["model_name"] = args.model + if args.pretrained: + model = model_from_name(args.model).from_pretrained(args.checkpoint_load, **model_kwargs) + else: + model = model_from_name(args.model)(**model_kwargs) # type: ignore + model.initialize() + # for model registry compliance + model_kwargs["model_name"] = args.model if args.wandb_log: - # wandb.init() wandb.config.update({"model_config": model_kwargs}) if args.quantization_scheduling: @@ -132,8 +135,8 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: ) wrapper_class = DistillationModelWrapper - if args.checkpoint_load is not None and args.pretrained: - logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") + if args.checkpoint_load is not None and args.resume_training: + logger.info(f"resuming training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = wrapper_class.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = wrapper_class(model, dataset.num_classes, quantization_scheduler, args) @@ -202,7 +205,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_wrapped, train_dataloaders=train_loader, val_dataloaders=test_loader, - ckpt_path=args.checkpoint_load if not args.pretrained else None, + ckpt_path=args.checkpoint_load if args.resume_training else None, ) diff --git a/examples/image_classification/requirements.txt b/examples/image_classification/requirements.txt index da0934b..3eb115f 100644 --- a/examples/image_classification/requirements.txt +++ b/examples/image_classification/requirements.txt @@ -1,5 +1,5 @@ bitorch fvbitcore -pytorch_lightning~=1.6.0 +pytorch_lightning>=1.8.1 sklearn wandb~=0.12.0 diff --git a/examples/image_classification/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py index 2433aef..3555e5d 100644 --- a/examples/image_classification/utils/arg_parser.py +++ b/examples/image_classification/utils/arg_parser.py @@ -9,8 +9,8 @@ from bitorch.models import model_from_name, model_names, Model from bitorch.models.base import NoArgparseArgsMixin from bitorch.quantizations.quantization_scheduler import Quantization_Scheduler -from ..datasets import dataset_names -from ..utils.teachers import available_teachers +from datasets import dataset_names +from utils.teachers import available_teachers class _HeadArgumentParser(ArgumentParser): @@ -123,10 +123,15 @@ def add_checkpoint_args(parser: ArgumentParser) -> None: default=None, help="path to checkpoint file to load state from. if omitted, a new model will be trained.", ) + checkpoint.add_argument( + "--resume_training", + action="store_true", + help="resume training from given checkpoint", + ) checkpoint.add_argument( "--pretrained", action="store_true", - help="uses the given checkpoint as a pretrained model (only for initialization)", + help="load the state dict either from model hub or from checkpoint_load", ) diff --git a/examples/image_classification/utils/callbacks.py b/examples/image_classification/utils/callbacks.py index bdbdb3f..4bff90c 100644 --- a/examples/image_classification/utils/callbacks.py +++ b/examples/image_classification/utils/callbacks.py @@ -8,7 +8,7 @@ class ProgressiveSignScalerCallback(pl.callbacks.Callback): """Callback that updates the scale of progressive sign functions based on current epoch.""" - def on_epoch_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: scale = trainer.current_epoch / trainer.max_epochs progressive_sign_config.progressive_sign_scale = scale for logger in trainer.loggers: diff --git a/examples/image_classification/utils/lightning_model.py b/examples/image_classification/utils/lightning_model.py index 4dbb0d8..85f3376 100644 --- a/examples/image_classification/utils/lightning_model.py +++ b/examples/image_classification/utils/lightning_model.py @@ -111,7 +111,7 @@ def on_validation_epoch_end(self) -> None: "quantization_scheduler/factor", self.quantization_scheduler.scheduled_quantizer_instances[0].factor, ) - return super().on_epoch_end() + return super().on_validation_epoch_end() def configure_optimizers(self) -> Union[dict, torch.optim.Optimizer]: # type: ignore logging.info(f"Using {self.hparams.optimizer} optimizer and {self.hparams.lr_scheduler} lr scheduler...") diff --git a/scripts/config.json b/scripts/config.json index b9acd27..4004c32 100644 --- a/scripts/config.json +++ b/scripts/config.json @@ -1,6 +1,9 @@ { "entity": "hpi-deep-learning", "project": "bitorch", - "compare_metrics": ["metrics/best-test-top1-accuracy/max"], + "compare_metrics": [ + "metrics/best-test-top1-accuracy/max", + "balanced accuracy/max" + ], "runs": null } \ No newline at end of file diff --git a/scripts/sync_wandb_nextcloud_hub.py b/scripts/sync_model_hub.py similarity index 67% rename from scripts/sync_wandb_nextcloud_hub.py rename to scripts/sync_model_hub.py index 70c35c2..7d3c9b9 100644 --- a/scripts/sync_wandb_nextcloud_hub.py +++ b/scripts/sync_model_hub.py @@ -1,3 +1,5 @@ +import numbers +import csv import sys from tqdm import tqdm import numpy as np @@ -10,52 +12,21 @@ import logging import argparse import datetime -from typing import Any, Optional +from typing import Any, Optional, Union from bitorch.models import model_from_name from bitorch.models.base import Model +from bitorch.models.model_hub import download_version_table, convert_dtypes from examples.image_classification.datasets import dataset_from_name +from examples.image_classification.utils.utils import configure_logging # from examples.image_classification.utils.arg_parser import create_argparser from importlib import import_module warnings.filterwarnings("ignore") INFINITY = 1e4 -def configure_logging(logger: Any, log_file: Optional[str], log_level: str, output_stdout: bool) -> None: - """configures logging module. - - Args: - logger: the logger to be configured - log_file (str): path to log file. if omitted, logging will be forced to stdout. - log_level (str): string name of log level (e.g. 'debug') - output_stdout (bool): toggles stdout output. will be activated automatically if no log file was given. - otherwise if activated, logging will be outputed both to stdout and log file. - """ - log_level_name = log_level.upper() - log_level = getattr(logging, log_level_name) - logger.setLevel(log_level) - - logging_format = logging.Formatter( - "%(asctime)s - %(levelname)s [%(filename)s : %(funcName)s() : l. %(lineno)s]: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - if log_file is not None: - log_file_path = Path(log_file) - log_file_path.parent.mkdir(parents=True, exist_ok=True) - file_handler = logging.FileHandler(log_file_path) - file_handler.setFormatter(logging_format) - logger.addHandler(file_handler) - else: - output_stdout = True - - if output_stdout: - stream = logging.StreamHandler() - stream.setFormatter(logging_format) - logger.addHandler(stream) - -def add_missing_columns(*key_lists, init_values=None, table): +def add_missing_columns(*key_lists: list, init_values: Any = None, table: pandas.DataFrame) -> pandas.DataFrame: for list_idx, key_list in enumerate(key_lists): for key_idx, key in enumerate(key_list): if key not in table.columns: @@ -71,7 +42,7 @@ def add_missing_columns(*key_lists, init_values=None, table): table[key] = value return table -def extract_model_parameters(run): +def extract_model_parameters(run: Any) -> dict: if "model_config" in run.config.keys(): model_kwargs = run.config["model_config"] else: @@ -90,8 +61,12 @@ def extract_model_parameters(run): metadata = json.load(metadata_file) if "dlrm" in metadata["program"]: + if "examples/dlrm" not in sys.path: + sys.path.append("examples/dlrm") create_argparser = import_module("examples.dlrm.utils.arg_parser").create_argparser else: + if "examples/image_classification" not in sys.path: + sys.path.append("examples/image_classification") create_argparser = import_module("examples.image_classification.utils.arg_parser").create_argparser parser, model_parser = create_argparser(metadata["args"]) @@ -112,66 +87,45 @@ def extract_model_parameters(run): logging.debug(f"extracted model config: {model_kwargs}") return model_kwargs -def extract_artifact_version(artifact, model_name): + +def extract_artifact_version(artifact: wandb.Artifact, model_name: str) -> Union[None, int]: for alias in artifact._attrs["aliases"]: if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): return int(alias["alias"][1:]) - -def download_version_table(api): - model_table_path = Model.version_table_path - try: - # os.system(f"curl -T /tmp/version_table.csv {model_table_url}") - model_table = api.artifact(f"{model_table_path}:latest").get_path(f"versions.csv").download(root="/tmp") - version_table = pandas.read_csv(model_table) - except Exception as e: - logging.info(f"could not retrieve model version table from {model_table_path}: {e}!") - return pandas.DataFrame() - - return version_table - -def upload_model_to_registry(run, model_name, api): - model_registry_path = f"{model_from_name(model_name).model_registry_base_path}/{model_name}" +def upload_model_to_hub(run: Any, model_name: str, api: wandb.Api) -> Union[None, int]: + model_hub_path = f"{model_from_name(model_name).model_hub_base_path}/{model_name}" run_artifact = run.logged_artifacts()[0] - run_artifact.link(model_registry_path) - uploaded_artifact = api.artifact(f"{model_registry_path}:latest") + run_artifact.link(model_hub_path) + uploaded_artifact = api.artifact(f"{model_hub_path}:latest") return extract_artifact_version(uploaded_artifact, model_name) -def compare(configA, configB, compare_metrics): +def compare(configA: dict, configB: dict, compare_metrics: list) -> bool: for metric_name, mode in compare_metrics: - if configA[metric_name] == configB[metric_name]: + if metric_name not in configA or metric_name not in configB or configA[metric_name] == configB[metric_name]: continue # this should be correct... return (configA[metric_name] < configB[metric_name]) == (mode == "max") - -def convert_dtypes(data): - for column in data.columns: - if data[column].dtype.kind not in 'biufc': - data[column] = data[column].astype(str) - return data + return False def add_model_to_version_table(model_kwargs, version_table, run, api): - model_kwargs["model_registry_version"] = upload_model_to_registry(run, model_kwargs["model_name"], api) - df_to_add = pandas.DataFrame([model_kwargs]) - # df_to_add = pandas.DataFrame([model_kwargs], dtype="string").apply(pandas.to_numeric, errors="ignore") - return pandas.concat([version_table, df_to_add], ignore_index=True) + model_kwargs["model_hub_version"] = upload_model_to_hub(run, model_kwargs["model_name"], api) + df_to_add = pandas.DataFrame([model_kwargs]) + return pandas.concat([version_table, df_to_add], ignore_index=True) def update_table(version_table, model_kwargs, run, compare_metrics, api): model_comparison_keys = list(model_kwargs.keys()) model_kwargs = convert_dtypes(model_kwargs) version_table = add_missing_columns( - model_kwargs.keys(), dict(run.summary).keys(), [metric_name for metric_name, _ in compare_metrics], ["time uploaded", "model_registry_version"], + model_kwargs.keys(), dict(run.summary).keys(), [metric_name for metric_name, _ in compare_metrics], ["time uploaded", "model_hub_version"], init_values=[None, None, [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], ["", "latest"]], table=version_table ) - print("comparing ", model_kwargs) - # this removes a deprecation warning - # model_kwargs_series = pandas.Series(model_kwargs, dtype="string").apply(pandas.to_numeric, errors="ignore") + logging.info(f"extracted model config: {model_kwargs}") model_kwargs_series = pandas.Series(model_kwargs) existing_row = version_table[(version_table[model_comparison_keys] == model_kwargs_series).all(1)] - # model_kwargs_series, version_table = model_kwargs_series.align(version_table, axis=0, copy=False) model_kwargs.update(dict(run.summary)) @@ -184,7 +138,7 @@ def update_table(version_table, model_kwargs, run, compare_metrics, api): existing_row_idx = np.where((version_table[model_comparison_keys] == model_kwargs_series).all(1))[0][0] existing_model_kwargs = version_table.iloc[existing_row_idx].to_dict() - # this prevents reuploading the same model + # this prevents reuploading the same model by favoring the older model if the other metrics are the same compare_metrics.append(["time uploaded", "min"]) new_model_better = compare(existing_model_kwargs, model_kwargs, compare_metrics) if new_model_better: @@ -197,7 +151,7 @@ def update_table(version_table, model_kwargs, run, compare_metrics, api): def write_table(version_table, model_name, api): - version_table.to_csv(f"/tmp/versions.csv") + version_table.to_csv(f"/tmp/versions.csv", quoting=csv.QUOTE_NONNUMERIC, index=False) entity, project, _ = model_from_name(model_name).version_table_path.split("/") with wandb.init(entity=entity, project=project) as run: version_table_artifact = wandb.Artifact("model-tables", type="tables") @@ -241,23 +195,23 @@ def main(args): continue compare_metrics.append([metric_name, mode]) - version_table = download_version_table(api) + version_table = download_version_table(Model.version_table_path, api, no_exception=True) for run in tqdm(runs): - # try: - if len(run.logged_artifacts()) == 0: - logging.info(f"run {run.name} has no logged artifacts, skipping...") - continue - model_kwargs = extract_model_parameters(run) - model_name = model_kwargs["model_name"] - version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) - # except Exception as e: - # logging.info(f"run {run.name} cannot be synced with registry due to error: {e}. skipping...") + try: + if len(run.logged_artifacts()) == 0: + logging.info(f"run {run.name} has no logged artifacts, skipping...") + continue + model_kwargs = extract_model_parameters(run) + model_name = model_kwargs["model_name"] + version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) + except Exception as e: + logging.info(f"run {run.name} cannot be synced with hub due to error: {e}. skipping...") write_table(version_table, model_name, api) logging.info(f"Successfully synced {len(runs)} runs!") -def delete_model_version_table_in_registry(): - logging.warn("DELETING MODEL VERSION TABLE IN REGISTRY...") +def delete_model_version_table_in_hub(): + logging.warn("DELETING MODEL VERSION TABLE IN HUB...") input("PRESS ENTER IF YOU ARE SURE YOU WANT TO CONTINUE:") entity, project, _ = Model.version_table_path.split("/") with wandb.init(entity=entity, project=project) as run: @@ -278,10 +232,10 @@ def delete_model_version_table_in_registry(): parser.add_argument("--entity", "-e", default=None, type=str) parser.add_argument("--project", "-p", default=None, type=str) parser.add_argument("--delete-version-table", default=False, action="store_true", - help="deletes the remote version table on the model registry. Use with caution!") + help="deletes the remote version table on the model hub. Use with caution!") args = parser.parse_args() if args.delete_version_table: - delete_model_version_table_in_registry() + delete_model_version_table_in_hub() sys.exit(0) main(args) From 47102c551bb669c0ad25900424d4dd234530f922 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 2 Dec 2022 14:49:52 +0100 Subject: [PATCH 193/208] added pretraining options to dlrm --- examples/dlrm/train_dlrm.py | 15 +++++++++------ examples/dlrm/utils/arg_parser.py | 9 +++++++-- examples/image_classification/utils/arg_parser.py | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index 7c7b138..b8ace89 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -156,18 +156,21 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs["embedding_layer_sizes"] = embedding_layer_sizes model_kwargs["input_shape"] = [] model_kwargs["dense_feature_size"] = dense_feature_size - - model = DLRM(**model_kwargs) # type: ignore - model.initialize() - if args.checkpoint_load is not None and args.pretrained: - logger.info(f"starting training from pretrained model at checkpoint {args.checkpoint_load}") + if args.pretrained: + model = DLRM.from_pretrained(args.checkpoint_load, **model_kwargs) + else: + model = DLRM(**model_kwargs) # type: ignore + model.initialize() + + if args.checkpoint_load is not None and args.resume_training: + logger.info(f"resuming training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = ModelWrapper(model, 1, args) # for model registry compliance - model_kwargs["model_name"] = args.model + model_kwargs["model_name"] = "dlrm" if args.wandb_log: wandb.config.update({"model_config": model_kwargs}) diff --git a/examples/dlrm/utils/arg_parser.py b/examples/dlrm/utils/arg_parser.py index ff572d9..0b2867c 100644 --- a/examples/dlrm/utils/arg_parser.py +++ b/examples/dlrm/utils/arg_parser.py @@ -102,12 +102,17 @@ def add_checkpoint_args(parser: ArgumentParser) -> None: "--checkpoint-load", type=str, default=None, - help="path to checkpoint file to load state from. if omitted, a new model will be trained.", + help="path to checkpoint file to load state from. if omitted and --pretrained is activated, the model weights will be downloaded from the model hub. If --pretrained is not set, a new model will be trained.", + ) + checkpoint.add_argument( + "--resume_training", + action="store_true", + help="resume training from given checkpoint", ) checkpoint.add_argument( "--pretrained", action="store_true", - help="uses the given checkpoint as a pretrained model (only for initialization)", + help="load the state dict either from model hub or from checkpoint_load", ) diff --git a/examples/image_classification/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py index 3555e5d..f422ae4 100644 --- a/examples/image_classification/utils/arg_parser.py +++ b/examples/image_classification/utils/arg_parser.py @@ -121,7 +121,7 @@ def add_checkpoint_args(parser: ArgumentParser) -> None: "--checkpoint-load", type=str, default=None, - help="path to checkpoint file to load state from. if omitted, a new model will be trained.", + help="path to checkpoint file to load state from. if omitted and --pretrained is activated, the model weights will be downloaded from the model hub. If --pretrained is not set, a new model will be trained.", ) checkpoint.add_argument( "--resume_training", From 67f480b825c05df32f874a7991da30da29164e66 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 2 Dec 2022 15:14:41 +0100 Subject: [PATCH 194/208] made black and mypy happy, flake soon to follow... --- bitorch/models/base.py | 26 +-- bitorch/models/model_hub.py | 97 +++++++++-- bitorch/util.py | 12 -- examples/dlrm/train_dlrm.py | 7 +- .../image_classification.py | 8 +- mypy.ini | 2 +- scripts/sync_model_hub.py | 160 +++++++++++------- test.py | 76 --------- tests/layers/test_layer_implementation.py | 2 +- 9 files changed, 199 insertions(+), 191 deletions(-) delete mode 100644 test.py diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 7eac74b..e7a2a8a 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -92,13 +92,11 @@ def initialize(self) -> None: def convert(self, new_mode: RuntimeMode, device: Optional[torch.device] = None, verbose: bool = False) -> "Model": return convert(self, new_mode, device, verbose) - def _generate_checkpoint_name(self): - # TODO: encode current runtime / layer implementation in name for better reference / correct loading of model - return f"{self.name}_checkpoint.pth" - @classmethod - def from_pretrained(cls, source: Optional[str] = None, mode: RuntimeMode = RuntimeMode.DEFAULT, **kwargs) -> nn.Module: - model = cls(**kwargs) + def from_pretrained( + cls, source: Optional[str] = None, mode: RuntimeMode = RuntimeMode.DEFAULT, **kwargs: str + ) -> nn.Module: + model = cls(**kwargs) # type: ignore if source is not None: logging.info(f"Loading {cls.name} model state_dict from file {source}") state_dict = torch.load(source) @@ -106,23 +104,9 @@ def from_pretrained(cls, source: Optional[str] = None, mode: RuntimeMode = Runti kwargs["model_name"] = cls.name.lower() logging.info(f"Downloading {cls.name} model state_dict from hub...") state_dict = load_from_hub(cls.version_table_path, cls.model_hub_base_path, **kwargs) - + model.load_state_dict(state_dict) return model - - - - def store_checkpoint(self, destination: Optional[str] = None) -> None: - checkpoint_name = self._generate_checkpoint_name() - if is_url(destination): - logging.info(f"uploading model to url: {destination}...") - tmp_path = f"/tmp/{checkpoint_name}" - torch.save(self, tmp_path) - self._upload_model(tmp_path, destination) - else: - logging.debug(f"saving model to {destination}...") - torch.save(self, destination) - logging.debug("done saving model!") class NoArgparseArgsMixin: diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 6298824..3d69143 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -1,3 +1,4 @@ +from typing import Dict, Any import wandb import numbers import pandas @@ -5,7 +6,17 @@ import warnings import torch -def convert_dtypes(data): + +def convert_dtypes(data: dict) -> dict: + """converts types of the values of dict so that they can be easily compared accross + dataframes and csvs. converts all values that are not numerical to string. + + Args: + data (dict): dict with values to be converted + + Returns: + dict: dict with converted values + """ for key, value in data.items(): if isinstance(value, list): value = tuple(value) @@ -13,7 +24,19 @@ def convert_dtypes(data): data[key] = str(value) return data -def get_matching_row(version_table, model_kwargs): + +def get_matching_row(version_table: pandas.DataFrame, model_kwargs: dict) -> pandas.DataFrame: + """searches the version table dataframe for a row that matches model kwargs + + Args: + version_table (pandas.DataFrame): the dataframe to search in + model_kwargs (dict): the dict to search for. does not have to have key-value-pairs of each + column of version_table, i.e. can be subset + + Returns: + pandas.DataFrame: row with values in model_kwargs.keys() columns that are equal to model_kwargs values. + if not existent, returns an empty dataframe. + """ model_kwargs = convert_dtypes(model_kwargs) with warnings.catch_warnings(): model_kwargs_series = pandas.Series(model_kwargs) @@ -22,34 +45,86 @@ def get_matching_row(version_table, model_kwargs): return None return existing_row -def get_model_path(version_table, model_kwargs, model_hub_base_path): + +def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict, model_hub_base_path: str) -> str: + """finds the matching row for model_kwargs in version table and path to model artifact for given configuration + + Args: + version_table (pandas.DataFrame): version table with model configurations and corresponding model hub versions + model_kwargs (dict): model configuration to search for + model_hub_base_path (str): base path that contains model hub version of models + + Raises: + RuntimeError: thrown if no matching model can be found in version table + + Returns: + str: path to matching model hub artifact + """ matching_row = get_matching_row(version_table, model_kwargs) if matching_row is None: - raise RuntimeError(f"No matching model found in hub with configuration: {model_kwargs}! You can train" - " it yourself or try to load it from a local checkpoint!") + raise RuntimeError( + f"No matching model found in hub with configuration: {model_kwargs}! You can train" + " it yourself or try to load it from a local checkpoint!" + ) model_version = matching_row["model_hub_version"][0] return f"{model_hub_base_path}/{model_kwargs['model_name']}:v{model_version}" -def load_from_hub(model_version_table_path, model_hub_base_path, download_path="/tmp", **model_kwargs): + +def load_from_hub( + model_version_table_path: str, model_hub_base_path: str, download_path: str = "/tmp", **model_kwargs: str +) -> torch.Tensor: + """loads the model that matches the requested model configuration in model_kwargs from the model hub. + + Args: + model_version_table_path (str): path to model version table on model hub + model_hub_base_path (str): base path for model hub for downloading stored models + download_path (str, optional): path to store the downloaded files. Defaults to "/tmp". + + Returns: + torch.Tensor: state dict of downloaded model file + """ api = wandb.Api() version_table = download_version_table(model_version_table_path, api) model_path = get_model_path(version_table, model_kwargs, model_hub_base_path) logging.info("downloading model...") downloaded_model = api.artifact(model_path).get_path("model.ckpt").download(root=download_path) artifact = torch.load(downloaded_model) - + # true if artifact is a checkpoint from pytorch lightning if isinstance(artifact, dict): - return lightning_checkpoint_to_state_dict(artifact) + return lightning_checkpoint_to_state_dict(artifact) # type: ignore return artifact -def lightning_checkpoint_to_state_dict(artifact): + +def lightning_checkpoint_to_state_dict(artifact: Dict[Any, Any]) -> Dict[Any, Any]: + """converts a pytorch lightning checkpoint to a normal torch state dict + + Args: + artifact (Dict[Any, Any]): dict containing a ['state_dict'] attribute + + Returns: + Dict[Any, Any]: state dict for model + """ state_dict = artifact["state_dict"] # turns model._model.arg keys in state dict into _model.arg extracted_state_dict = {key[6:]: value for key, value in state_dict.items()} return extracted_state_dict -def download_version_table(model_table_path, api, no_exception=False): + +def download_version_table(model_table_path: str, api: wandb.Api, no_exception: bool = False) -> pandas.DataFrame: + """downloads the newest version table from model hub. + + Args: + model_table_path (str): path on hub to model version table + api (wandb.Api): api to make download request with + no_exception (bool, optional): weather exception shall be thrown if received version table is empty. Defaults to False. + + Raises: + Exception: thrown if received version table is empty / cannot be downloaded and no_exception is False + + Returns: + pandas.DataFrame: model version table + """ logging.info("downloading model version table from hub...") try: model_table = api.artifact(f"{model_table_path}:latest").get_path(f"versions.csv").download(root="/tmp") @@ -60,4 +135,4 @@ def download_version_table(model_table_path, api, no_exception=False): logging.info("creating empty table...") return pandas.DataFrame() raise Exception(e) - return version_table \ No newline at end of file + return version_table diff --git a/bitorch/util.py b/bitorch/util.py index c2af955..955364b 100644 --- a/bitorch/util.py +++ b/bitorch/util.py @@ -1,4 +1,3 @@ -import re import typing import importlib from typing import Optional, Callable, List, Any, Dict @@ -41,14 +40,3 @@ def filter_fn(x: Any) -> bool: lookup[transformed_key] = class_ return lookup - - -def is_url(input_str: str) -> bool: - regex = re.compile( - r'^(?:http|ftp)s?://' - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' - r'localhost|' - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' - r'(?::\d+)?' - r'(?:/?|[/?]\S+)$', re.IGNORECASE) - return re.match(regex, input_str) is not None diff --git a/examples/dlrm/train_dlrm.py b/examples/dlrm/train_dlrm.py index b8ace89..7353c53 100644 --- a/examples/dlrm/train_dlrm.py +++ b/examples/dlrm/train_dlrm.py @@ -151,7 +151,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: print("DATA SHAPE:", (type(data_point[0]), (type(data_point[1]), type(data_point[2])))) print("DATA SHAPE:", (data_point[0].shape, (data_point[1].shape, data_point[2].shape))) data_point = (data_point[0], (data_point[1], data_point[2])) - + # for model registry compliance model_kwargs["embedding_layer_sizes"] = embedding_layer_sizes model_kwargs["input_shape"] = [] @@ -161,14 +161,13 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: else: model = DLRM(**model_kwargs) # type: ignore model.initialize() - + if args.checkpoint_load is not None and args.resume_training: logger.info(f"resuming training from pretrained model at checkpoint {args.checkpoint_load}") model_wrapped = ModelWrapper.load_from_checkpoint(args.checkpoint_load) else: model_wrapped = ModelWrapper(model, 1, args) - - + # for model registry compliance model_kwargs["model_name"] = "dlrm" if args.wandb_log: diff --git a/examples/image_classification/image_classification.py b/examples/image_classification/image_classification.py index eb9a940..51d6285 100644 --- a/examples/image_classification/image_classification.py +++ b/examples/image_classification/image_classification.py @@ -58,9 +58,9 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: loggers: List[LightningLoggerBase] = [] if args.tensorboard_log: - loggers.append(TensorBoardLogger(str(output_dir), name="tensorboard")) + loggers.append(TensorBoardLogger(str(output_dir), name="tensorboard")) # type: ignore if args.csv_log: - loggers.append(CSVLogger(str(output_dir), name="csv")) + loggers.append(CSVLogger(str(output_dir), name="csv")) # type: ignore if args.wandb_log: loggers.append( CustomWandbLogger( @@ -101,7 +101,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: model_kwargs = vars(model_args) logger.debug(f"got model args as dict: {model_kwargs}") - + model_kwargs["input_shape"] = tuple(dataset.shape) model_kwargs["num_classes"] = dataset.num_classes if args.pretrained: @@ -109,7 +109,7 @@ def main(args: argparse.Namespace, model_args: argparse.Namespace) -> None: else: model = model_from_name(args.model)(**model_kwargs) # type: ignore model.initialize() - + # for model registry compliance model_kwargs["model_name"] = args.model if args.wandb_log: diff --git a/mypy.ini b/mypy.ini index 177e95e..aad58c7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ [mypy] -files = bitorch, examples +files = bitorch, examples, scripts show_error_codes = True follow_imports = silent pretty = True diff --git a/scripts/sync_model_hub.py b/scripts/sync_model_hub.py index 7d3c9b9..3d3be73 100644 --- a/scripts/sync_model_hub.py +++ b/scripts/sync_model_hub.py @@ -20,34 +20,38 @@ from examples.image_classification.datasets import dataset_from_name from examples.image_classification.utils.utils import configure_logging + # from examples.image_classification.utils.arg_parser import create_argparser from importlib import import_module warnings.filterwarnings("ignore") INFINITY = 1e4 + def add_missing_columns(*key_lists: list, init_values: Any = None, table: pandas.DataFrame) -> pandas.DataFrame: for list_idx, key_list in enumerate(key_lists): for key_idx, key in enumerate(key_list): if key not in table.columns: value = ( - init_values - if not isinstance(init_values, list) - else ( - init_values[list_idx] - if not isinstance(init_values[list_idx], list) - else init_values[list_idx][key_idx] - ) + init_values + if not isinstance(init_values, list) + else ( + init_values[list_idx] + if not isinstance(init_values[list_idx], list) + else init_values[list_idx][key_idx] + ) ) table[key] = value return table + def extract_model_parameters(run: Any) -> dict: if "model_config" in run.config.keys(): model_kwargs = run.config["model_config"] else: logging.info( - "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command (experimental)...") + "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command (experimental)..." + ) logging.debug("downloading run metadata...") run.file("wandb-metadata.json").download("/tmp", replace=True) @@ -55,11 +59,11 @@ def extract_model_parameters(run: Any) -> dict: metadata_path = Path("/tmp/wandb-metadata.json") if not metadata_path.exists(): logging.error("metadata file could not be downloaded! skipping run...") - return + return {} with metadata_path.open() as metadata_file: metadata = json.load(metadata_file) - + if "dlrm" in metadata["program"]: if "examples/dlrm" not in sys.path: sys.path.append("examples/dlrm") @@ -68,7 +72,7 @@ def extract_model_parameters(run: Any) -> dict: if "examples/image_classification" not in sys.path: sys.path.append("examples/image_classification") create_argparser = import_module("examples.image_classification.utils.arg_parser").create_argparser - + parser, model_parser = create_argparser(metadata["args"]) parser.exit_on_error = False model_parser.exit_on_error = False @@ -88,18 +92,25 @@ def extract_model_parameters(run: Any) -> dict: return model_kwargs -def extract_artifact_version(artifact: wandb.Artifact, model_name: str) -> Union[None, int]: +def extract_artifact_version(artifact: wandb.Artifact, model_name: str) -> int: for alias in artifact._attrs["aliases"]: - if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): + if ( + alias["artifactCollectionName"] == model_name + and alias["alias"][0] == "v" + and alias["alias"][1:].isnumeric() + ): return int(alias["alias"][1:]) + return 0 + -def upload_model_to_hub(run: Any, model_name: str, api: wandb.Api) -> Union[None, int]: +def upload_model_to_hub(run: Any, model_name: str, api: wandb.Api) -> int: model_hub_path = f"{model_from_name(model_name).model_hub_base_path}/{model_name}" run_artifact = run.logged_artifacts()[0] run_artifact.link(model_hub_path) uploaded_artifact = api.artifact(f"{model_hub_path}:latest") return extract_artifact_version(uploaded_artifact, model_name) + def compare(configA: dict, configB: dict, compare_metrics: list) -> bool: for metric_name, mode in compare_metrics: if metric_name not in configA or metric_name not in configB or configA[metric_name] == configB[metric_name]: @@ -108,25 +119,37 @@ def compare(configA: dict, configB: dict, compare_metrics: list) -> bool: return (configA[metric_name] < configB[metric_name]) == (mode == "max") return False -def add_model_to_version_table(model_kwargs, version_table, run, api): + +def add_model_to_version_table( + model_kwargs: dict, version_table: pandas.DataFrame, run: Any, api: wandb.Api +) -> pandas.DataFrame: model_kwargs["model_hub_version"] = upload_model_to_hub(run, model_kwargs["model_name"], api) df_to_add = pandas.DataFrame([model_kwargs]) return pandas.concat([version_table, df_to_add], ignore_index=True) - -def update_table(version_table, model_kwargs, run, compare_metrics, api): + +def update_table( + version_table: pandas.DataFrame, model_kwargs: dict, run: Any, compare_metrics: list, api: wandb.Api +) -> pandas.DataFrame: model_comparison_keys = list(model_kwargs.keys()) model_kwargs = convert_dtypes(model_kwargs) version_table = add_missing_columns( - model_kwargs.keys(), dict(run.summary).keys(), [metric_name for metric_name, _ in compare_metrics], ["time uploaded", "model_hub_version"], - init_values=[None, None, [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], ["", "latest"]], - table=version_table + model_kwargs.keys(), # type: ignore + dict(run.summary).keys(), # type: ignore + [metric_name for metric_name, _ in compare_metrics], + ["time uploaded", "model_hub_version"], + init_values=[ + None, + None, + [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], + ["", "latest"], + ], + table=version_table, ) - + logging.info(f"extracted model config: {model_kwargs}") model_kwargs_series = pandas.Series(model_kwargs) existing_row = version_table[(version_table[model_comparison_keys] == model_kwargs_series).all(1)] - model_kwargs.update(dict(run.summary)) model_kwargs["time uploaded"] = str(datetime.datetime.now()) @@ -137,7 +160,7 @@ def update_table(version_table, model_kwargs, run, compare_metrics, api): else: existing_row_idx = np.where((version_table[model_comparison_keys] == model_kwargs_series).all(1))[0][0] existing_model_kwargs = version_table.iloc[existing_row_idx].to_dict() - + # this prevents reuploading the same model by favoring the older model if the other metrics are the same compare_metrics.append(["time uploaded", "min"]) new_model_better = compare(existing_model_kwargs, model_kwargs, compare_metrics) @@ -150,17 +173,47 @@ def update_table(version_table, model_kwargs, run, compare_metrics, api): return version_table -def write_table(version_table, model_name, api): +def write_table(version_table: pandas.DataFrame, model_name: str) -> None: version_table.to_csv(f"/tmp/versions.csv", quoting=csv.QUOTE_NONNUMERIC, index=False) entity, project, _ = model_from_name(model_name).version_table_path.split("/") - with wandb.init(entity=entity, project=project) as run: + with wandb.init(entity=entity, project=project) as run: # type: ignore version_table_artifact = wandb.Artifact("model-tables", type="tables") version_table_artifact.add_file("/tmp/versions.csv") run.log_artifact(version_table_artifact) run.link_artifact(version_table_artifact, model_from_name(model_name).version_table_path) -def main(args): +def parse_metrics(metrics: list) -> list: + compare_metrics = [] + for metric in metrics: + parts = metric.split("/") + metric_name = "/".join(parts[:-1]) + mode = parts[-1] + if metric_name is None or mode is None or mode not in ["min", "max"]: + logging.error( + f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max" + ) + continue + compare_metrics.append([metric_name, mode]) + return compare_metrics + + +def delete_model_version_table_in_hub() -> None: + logging.warn("DELETING MODEL VERSION TABLE IN HUB...") + input("PRESS ENTER IF YOU ARE SURE YOU WANT TO CONTINUE:") + + entity, project, _ = Model.version_table_path.split("/") + with wandb.init(entity=entity, project=project) as run: # type: ignore + table_art = wandb.Artifact("model-tables", type="tables") + with Path("versions.csv").open("w") as v: + v.write("") + table_art.add_file("versions.csv") + run.log_artifact(table_art) + run.link_artifact(table_art, Model.version_table_path) + logging.warn("MODEL VERSION TABLE DELETED!") + + +def main(args: argparse.Namespace) -> None: if args.config is not None: config_path = Path(args.config) if not config_path.exists(): @@ -169,32 +222,25 @@ def main(args): config = json.load(config_file) else: config = vars(args) - + configure_logging(logging.getLogger(), None, "info", True) - print("args:", args, "entity:", config["entity"]) api = wandb.Api() - filters = None if config["runs"] is None else {"$or": [{"config.experiment_name": run_name} for run_name in config["runs"]]} + filters = ( + None + if config["runs"] is None + else {"$or": [{"config.experiment_name": run_name} for run_name in config["runs"]]} + ) entity = config["entity"] project = config["project"] - print("path:", f"{entity}/{project}") + logging.info("Syncing runs from:", f"{entity}/{project}") runs = api.runs( path=f"{entity}/{project}", filters=filters, ) - metrics = config["compare_metrics"] - compare_metrics = [] - for metric in metrics: - parts = metric.split("/") - metric_name = "/".join(parts[:-1]) - mode = parts[-1] - if metric_name is None or mode is None or mode not in ["min", "max"]: - logging.error( - f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max") - continue - compare_metrics.append([metric_name, mode]) - + compare_metrics = parse_metrics(config["compare_metrics"]) + version_table = download_version_table(Model.version_table_path, api, no_exception=True) for run in tqdm(runs): try: @@ -207,34 +253,26 @@ def main(args): except Exception as e: logging.info(f"run {run.name} cannot be synced with hub due to error: {e}. skipping...") - write_table(version_table, model_name, api) + write_table(version_table, model_name) logging.info(f"Successfully synced {len(runs)} runs!") -def delete_model_version_table_in_hub(): - logging.warn("DELETING MODEL VERSION TABLE IN HUB...") - input("PRESS ENTER IF YOU ARE SURE YOU WANT TO CONTINUE:") - entity, project, _ = Model.version_table_path.split("/") - with wandb.init(entity=entity, project=project) as run: - table_art = wandb.Artifact("model-tables", type="tables") - with Path("versions.csv").open("w") as v: - v.write("") - table_art.add_file("versions.csv") - run.log_artifact(table_art) - run.link_artifact(table_art, Model.version_table_path) - logging.warn("MODEL VERSION TABLE DELETED!") - if __name__ == "__main__": parser = argparse.ArgumentParser("sync script of wandb runs and the model zoo") parser.add_argument("--config", "-c", default=None, type=str, help="path to config json file") - parser.add_argument("--runs", "-r", nargs="*", default=None, - help="the list of runs to sync. If omitted, all runs are synced.") + parser.add_argument( + "--runs", "-r", nargs="*", default=None, help="the list of runs to sync. If omitted, all runs are synced." + ) parser.add_argument("--entity", "-e", default=None, type=str) parser.add_argument("--project", "-p", default=None, type=str) - parser.add_argument("--delete-version-table", default=False, action="store_true", - help="deletes the remote version table on the model hub. Use with caution!") + parser.add_argument( + "--delete-version-table", + default=False, + action="store_true", + help="deletes the remote version table on the model hub. Use with caution!", + ) args = parser.parse_args() - + if args.delete_version_table: delete_model_version_table_in_hub() sys.exit(0) diff --git a/test.py b/test.py deleted file mode 100644 index 245cad4..0000000 --- a/test.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -import wandb - -# def extract_artifact_version(artifact, model_name): -# for alias in artifact._attrs["aliases"]: -# if alias["artifactCollectionName"] == model_name and alias["alias"][0] == "v" and alias["alias"][1:].isnumeric(): -# return int(alias["alias"][1:]) - -# def get_all_model_versions(entity, project, model_name): -# api = wandb.Api(overrides={"entity": entity, "project": project}) -# latest_model = api.artifact(f"{model_name}:latest", type="model") -# latest_version = extract_artifact_version(latest_model, model_name) - -# versions = [] -# for i in range(latest_version + 1): -# try: -# versions.append(api.artifact(f"{model_name}:v{i}", type="model")) -# except Exception as e: -# print(f"{model_name} version v{i} could not be retrieved! skipping...") -# pass - -# print(f"retrieved {len(versions)} model versions!") -# other_mdoel = wandb.Artifact(f"{entity}/{project}/{model_name}", type="model") -# other_mdoel.add_file("model.ckpt") -# other_mdoel.save() -# return versions - -# # run = wandb.init() -# # api = wandb.Api(overrides={"entity": "snagnar", "project": "model-registry"}) -# # artifact = run.use_artifact('snagnar/model-registry/bnn-model:v1', type='model') -# # artifact_dir = artifact.download() -# versions = get_all_model_versions("snagnar", "model-registry", "bnnmodel") - -# for version in versions: -# print(f"v{extract_artifact_version(version, 'bnnmodel')}") - -# # print("now melius") -# # versions = get_all_model_versions("hpi-deep-learning", "model-registry", "MeliusNet") - -# # for version in versions: -# # print(f"v{extract_artifact_version(version, 'MeliusNet')}") -# ... - -from pathlib import Path -import random -# # with wandb.init(entity="hpi-deep-learning", project="model-registry") as run: -with wandb.init(entity="hpi-deep-learning", project="model-registry") as run: - table_art = wandb.Artifact("model-tables", type="tables") - # with Path("versions.csv").open("w") as v: - # v.write(f"{random.randint(0, 1e6)}") - - # with Path("versions.csv").open() as v: - # print("v:", v.readlines()) - table_art.add_file("versions.csv") - # table_art.save() - # table_art.wait() - run.log_artifact(table_art) - # synced_table_art = run.logged_artifacts()[0] - run.link_artifact(table_art, "hpi-deep-learning/model-registry/model-version-tables") - -# table_art.link("hpi-deep-learning/model-registry/model-version-tables") - -# import wandb -# Initialize a W&B run to start tracking -# wandb.init() - -# # Create an Model Version -# art = wandb.Artifact("model-tables", type="tables") - -# art.add_file("versions.csv") - -# # Log the Model Version -# wandb.log_artifact(art) - -# # Link the Model Version to the Collection -# wandb.run.link_artifact(art, "model-registry/bnnmodel") \ No newline at end of file diff --git a/tests/layers/test_layer_implementation.py b/tests/layers/test_layer_implementation.py index e4ec7f2..10c752a 100644 --- a/tests/layers/test_layer_implementation.py +++ b/tests/layers/test_layer_implementation.py @@ -94,7 +94,7 @@ def test_default_impl(): print(layer) # TODO: pickling is currently only possible in RAW mode content = pickle.dumps(layer) - + layer_loaded = pickle.loads(content) assert layer_loaded.val == 21 From 6bb0d6c2a65a493ec2aa257e4ef99b2f532506f4 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 2 Dec 2022 15:15:13 +0100 Subject: [PATCH 195/208] removed unused imports --- bitorch/models/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index e7a2a8a..4ece854 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -1,4 +1,3 @@ -import pandas import logging from argparse import ArgumentParser from typing import Optional, List, Any @@ -11,9 +10,7 @@ from bitorch.layers.qconv1d import QConv1dBase, QConv1d_NoAct from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct from bitorch.layers.qconv3d import QConv3dBase, QConv3d_NoAct -from bitorch.util import is_url from bitorch.models.model_hub import load_from_hub -import wandb class Model(nn.Module): From 301a85f01500683fe4279b53b5bbb53ecd99125a Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 6 Jan 2023 13:14:03 +0100 Subject: [PATCH 196/208] some fixes --- bitorch/models/base.py | 8 ++++---- bitorch/models/model_hub.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index e7a2a8a..6d5b51b 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -11,17 +11,17 @@ from bitorch.layers.qconv1d import QConv1dBase, QConv1d_NoAct from bitorch.layers.qconv2d import QConv2dBase, QConv2d_NoAct from bitorch.layers.qconv3d import QConv3dBase, QConv3d_NoAct -from bitorch.util import is_url from bitorch.models.model_hub import load_from_hub -import wandb class Model(nn.Module): """Base class for Bitorch models""" name = "" - version_table_path = "hpi-deep-learning/model-registry/model-version-tables" - model_hub_base_path = "hpi-deep-learning/model-registry" + version_table_path = "snagnar/model-registry/model-version-tables" + model_hub_base_path = "snagnar/model-registry" + # version_table_path = "hpi-deep-learning/model-registry/model-version-tables" + # model_hub_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 3d69143..de884c0 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -88,7 +88,8 @@ def load_from_hub( model_path = get_model_path(version_table, model_kwargs, model_hub_base_path) logging.info("downloading model...") downloaded_model = api.artifact(model_path).get_path("model.ckpt").download(root=download_path) - artifact = torch.load(downloaded_model) + logging.info("Model downloaded!") + artifact = torch.load(downloaded_model, map_location="cpu") # true if artifact is a checkpoint from pytorch lightning if isinstance(artifact, dict): From d2742b71bcd2283be06c7179c34f8629ba82bf51 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 6 Jan 2023 13:15:39 +0100 Subject: [PATCH 197/208] fix attempt for faulty visibility settings --- bitorch/models/base.py | 6 ++++-- bitorch/models/model_hub.py | 6 +++--- scripts/sync_model_hub.py | 26 +++++++++++++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 4ece854..8648807 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -17,8 +17,10 @@ class Model(nn.Module): """Base class for Bitorch models""" name = "" - version_table_path = "hpi-deep-learning/model-registry/model-version-tables" - model_hub_base_path = "hpi-deep-learning/model-registry" + version_table_path = "snagnar/model-registry/model-version-tables" + model_hub_base_path = "snagnar/model-registry" + # version_table_path = "hpi-deep-learning/model-registry/model-version-tables" + # model_hub_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 3d69143..7a73bec 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -8,7 +8,7 @@ def convert_dtypes(data: dict) -> dict: - """converts types of the values of dict so that they can be easily compared accross + """converts types of the values of dict so that they can be easily compared accross dataframes and csvs. converts all values that are not numerical to string. Args: @@ -29,7 +29,7 @@ def get_matching_row(version_table: pandas.DataFrame, model_kwargs: dict) -> pan """searches the version table dataframe for a row that matches model kwargs Args: - version_table (pandas.DataFrame): the dataframe to search in + version_table (pandas.DataFrame): the dataframe to search in model_kwargs (dict): the dict to search for. does not have to have key-value-pairs of each column of version_table, i.e. can be subset @@ -127,7 +127,7 @@ def download_version_table(model_table_path: str, api: wandb.Api, no_exception: """ logging.info("downloading model version table from hub...") try: - model_table = api.artifact(f"{model_table_path}:latest").get_path(f"versions.csv").download(root="/tmp") + model_table = api.artifact(f"{model_table_path}:latest").get_path("versions.csv").download(root="/tmp") version_table = pandas.read_csv(model_table) except Exception as e: logging.info(f"could not retrieve model version table from {model_table_path}: {e}") diff --git a/scripts/sync_model_hub.py b/scripts/sync_model_hub.py index 3d3be73..cdc5542 100644 --- a/scripts/sync_model_hub.py +++ b/scripts/sync_model_hub.py @@ -29,6 +29,15 @@ def add_missing_columns(*key_lists: list, init_values: Any = None, table: pandas.DataFrame) -> pandas.DataFrame: + """adds missing columns to the dataframe as specified by the key lists. default values can be passed with init values. + + Args: + table (pandas.DataFrame): dataframe that receives the new columns + init_values (Any, optional): standard values the columns. Defaults to None. + + Returns: + pandas.DataFrame: upated dataframe + """ for list_idx, key_list in enumerate(key_lists): for key_idx, key in enumerate(key_list): if key not in table.columns: @@ -105,10 +114,17 @@ def extract_artifact_version(artifact: wandb.Artifact, model_name: str) -> int: def upload_model_to_hub(run: Any, model_name: str, api: wandb.Api) -> int: model_hub_path = f"{model_from_name(model_name).model_hub_base_path}/{model_name}" - run_artifact = run.logged_artifacts()[0] - run_artifact.link(model_hub_path) - uploaded_artifact = api.artifact(f"{model_hub_path}:latest") - return extract_artifact_version(uploaded_artifact, model_name) + entity, project = model_from_name(model_name).model_hub_base_path.split("/") + with wandb.init(entity=entity, project=project, name=model_name) as model_registry_run: + run_artifact = run.logged_artifacts()[0] + run_artifact.get_path("model.ckpt").download("/tmp") + model_registry_artifact = wandb.Artifact(model_name, type="pretrained_model") + model_registry_artifact.add_file("/tmp/model.ckpt") + model_registry_run.log_artifact(model_registry_artifact) + # run_artifact.link(model_hub_path) + # uploaded_artifact = api.artifact(f"{model_hub_path}:latest") + model_registry_run.link_artifact(model_registry_artifact, model_hub_path) + return extract_artifact_version(model_registry_artifact, model_name) def compare(configA: dict, configB: dict, compare_metrics: list) -> bool: @@ -233,7 +249,7 @@ def main(args: argparse.Namespace) -> None: ) entity = config["entity"] project = config["project"] - logging.info("Syncing runs from:", f"{entity}/{project}") + logging.info(f"Syncing runs from: {entity}/{project}") runs = api.runs( path=f"{entity}/{project}", filters=filters, From 23f1acef7695b111e179e745e288d6b1035907f3 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 6 Jan 2023 17:32:15 +0100 Subject: [PATCH 198/208] removed sync scripts --- bitorch/models/base.py | 8 +- bitorch/models/model_hub.py | 54 ++++--- scripts/config.json | 9 -- scripts/sync_model_hub.py | 295 ------------------------------------ 4 files changed, 40 insertions(+), 326 deletions(-) delete mode 100644 scripts/config.json delete mode 100644 scripts/sync_model_hub.py diff --git a/bitorch/models/base.py b/bitorch/models/base.py index 8648807..500c9eb 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -17,10 +17,8 @@ class Model(nn.Module): """Base class for Bitorch models""" name = "" - version_table_path = "snagnar/model-registry/model-version-tables" - model_hub_base_path = "snagnar/model-registry" - # version_table_path = "hpi-deep-learning/model-registry/model-version-tables" - # model_hub_base_path = "hpi-deep-learning/model-registry" + version_table_url = "https://api.wandb.ai/artifactsV2/default/hpi-deep-learning/QXJ0aWZhY3Q6MzE1MzQ1ODM1/a9bd2573417efc7fb8f562f06f3d322d" + model_hub_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() @@ -102,7 +100,7 @@ def from_pretrained( else: kwargs["model_name"] = cls.name.lower() logging.info(f"Downloading {cls.name} model state_dict from hub...") - state_dict = load_from_hub(cls.version_table_path, cls.model_hub_base_path, **kwargs) + state_dict = load_from_hub(cls.version_table_url, **kwargs) model.load_state_dict(state_dict) return model diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index a8b2f84..7539b5e 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -1,11 +1,24 @@ -from typing import Dict, Any -import wandb +from pathlib import Path +from typing import Dict, Any, Union +import os import numbers import pandas import logging import warnings import torch +import base64 +import hashlib +def md5_hash_file(path: Path): + hash_md5 = hashlib.md5() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(64 * 1024), b""): + hash_md5.update(chunk) + return hash_md5 + +def digest_file(path: Union[Path, str]): + path = Path(path) + return base64.b64encode(md5_hash_file(path).digest()).decode("ascii") def convert_dtypes(data: dict) -> dict: """converts types of the values of dict so that they can be easily compared accross @@ -46,7 +59,7 @@ def get_matching_row(version_table: pandas.DataFrame, model_kwargs: dict) -> pan return existing_row -def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict, model_hub_base_path: str) -> str: +def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict) -> str: """finds the matching row for model_kwargs in version table and path to model artifact for given configuration Args: @@ -66,12 +79,12 @@ def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict, model_hu f"No matching model found in hub with configuration: {model_kwargs}! You can train" " it yourself or try to load it from a local checkpoint!" ) - model_version = matching_row["model_hub_version"][0] - return f"{model_hub_base_path}/{model_kwargs['model_name']}:v{model_version}" - + model_url = matching_row["model_hub_url"][0] + model_digest = matching_row["model_digest"][0] + return model_url, model_digest def load_from_hub( - model_version_table_path: str, model_hub_base_path: str, download_path: str = "/tmp", **model_kwargs: str + model_version_table_path: str, download_path: str = "bitorch_models", **model_kwargs: str ) -> torch.Tensor: """loads the model that matches the requested model configuration in model_kwargs from the model hub. @@ -83,13 +96,20 @@ def load_from_hub( Returns: torch.Tensor: state dict of downloaded model file """ - api = wandb.Api() - version_table = download_version_table(model_version_table_path, api) - model_path = get_model_path(version_table, model_kwargs, model_hub_base_path) - logging.info("downloading model...") - downloaded_model = api.artifact(model_path).get_path("model.ckpt").download(root=download_path) - logging.info("Model downloaded!") - artifact = torch.load(downloaded_model, map_location="cpu") + Path(download_path).mkdir(parents=True, exist_ok=True) + + version_table = download_version_table(model_version_table_path) + model_path, model_digest = get_model_path(version_table, model_kwargs) + model_checksum = model_path.split("/")[-1] + model_local_path = Path(f"{download_path}/{model_checksum}") + + if(not model_local_path.exists() or digest_file(str(model_local_path)) != model_digest): + logging.info("downloading model...") + os.system(f"wget {model_path} -O {str(model_local_path)} -q --show-progress") + logging.info("Model downloaded!") + else: + logging.info(f"Using already downloaded model at {model_local_path}") + artifact = torch.load(model_local_path, map_location="cpu") # true if artifact is a checkpoint from pytorch lightning if isinstance(artifact, dict): @@ -112,7 +132,7 @@ def lightning_checkpoint_to_state_dict(artifact: Dict[Any, Any]) -> Dict[Any, An return extracted_state_dict -def download_version_table(model_table_path: str, api: wandb.Api, no_exception: bool = False) -> pandas.DataFrame: +def download_version_table(model_table_path: str, no_exception: bool = False) -> pandas.DataFrame: """downloads the newest version table from model hub. Args: @@ -128,8 +148,8 @@ def download_version_table(model_table_path: str, api: wandb.Api, no_exception: """ logging.info("downloading model version table from hub...") try: - model_table = api.artifact(f"{model_table_path}:latest").get_path("versions.csv").download(root="/tmp") - version_table = pandas.read_csv(model_table) + os.system(f"wget {model_table_path} -O /tmp/bitorch_model_version_table.csv -q --show-progress") + version_table = pandas.read_csv("/tmp/bitorch_model_version_table.csv") except Exception as e: logging.info(f"could not retrieve model version table from {model_table_path}: {e}") if no_exception: diff --git a/scripts/config.json b/scripts/config.json deleted file mode 100644 index 4004c32..0000000 --- a/scripts/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "entity": "hpi-deep-learning", - "project": "bitorch", - "compare_metrics": [ - "metrics/best-test-top1-accuracy/max", - "balanced accuracy/max" - ], - "runs": null -} \ No newline at end of file diff --git a/scripts/sync_model_hub.py b/scripts/sync_model_hub.py deleted file mode 100644 index cdc5542..0000000 --- a/scripts/sync_model_hub.py +++ /dev/null @@ -1,295 +0,0 @@ -import numbers -import csv -import sys -from tqdm import tqdm -import numpy as np -import warnings -import pandas -import os -from pathlib import Path -import json -import wandb -import logging -import argparse -import datetime -from typing import Any, Optional, Union - -from bitorch.models import model_from_name -from bitorch.models.base import Model -from bitorch.models.model_hub import download_version_table, convert_dtypes - -from examples.image_classification.datasets import dataset_from_name -from examples.image_classification.utils.utils import configure_logging - -# from examples.image_classification.utils.arg_parser import create_argparser -from importlib import import_module - -warnings.filterwarnings("ignore") -INFINITY = 1e4 - - -def add_missing_columns(*key_lists: list, init_values: Any = None, table: pandas.DataFrame) -> pandas.DataFrame: - """adds missing columns to the dataframe as specified by the key lists. default values can be passed with init values. - - Args: - table (pandas.DataFrame): dataframe that receives the new columns - init_values (Any, optional): standard values the columns. Defaults to None. - - Returns: - pandas.DataFrame: upated dataframe - """ - for list_idx, key_list in enumerate(key_lists): - for key_idx, key in enumerate(key_list): - if key not in table.columns: - value = ( - init_values - if not isinstance(init_values, list) - else ( - init_values[list_idx] - if not isinstance(init_values[list_idx], list) - else init_values[list_idx][key_idx] - ) - ) - table[key] = value - return table - - -def extract_model_parameters(run: Any) -> dict: - if "model_config" in run.config.keys(): - model_kwargs = run.config["model_config"] - else: - logging.info( - "DEPRECATED: No model_config entry in run config found! Trying to reconstruct model parameters by parsing intial run command (experimental)..." - ) - - logging.debug("downloading run metadata...") - run.file("wandb-metadata.json").download("/tmp", replace=True) - - metadata_path = Path("/tmp/wandb-metadata.json") - if not metadata_path.exists(): - logging.error("metadata file could not be downloaded! skipping run...") - return {} - - with metadata_path.open() as metadata_file: - metadata = json.load(metadata_file) - - if "dlrm" in metadata["program"]: - if "examples/dlrm" not in sys.path: - sys.path.append("examples/dlrm") - create_argparser = import_module("examples.dlrm.utils.arg_parser").create_argparser - else: - if "examples/image_classification" not in sys.path: - sys.path.append("examples/image_classification") - create_argparser = import_module("examples.image_classification.utils.arg_parser").create_argparser - - parser, model_parser = create_argparser(metadata["args"]) - parser.exit_on_error = False - model_parser.exit_on_error = False - - args_, unparsed_model_args = parser.parse_known_args(metadata["args"]) - model_args_, _ = model_parser.parse_known_args(unparsed_model_args) - - model_kwargs = vars(model_args_) - if not "dlrm" in metadata["program"]: - dataset = dataset_from_name(args_.dataset) - model_kwargs["input_shape"] = dataset.shape - model_kwargs["num_classes"] = dataset.num_classes - model_kwargs["model_name"] = args_.model - else: - model_kwargs["model_name"] = "dlrm" - logging.debug(f"extracted model config: {model_kwargs}") - return model_kwargs - - -def extract_artifact_version(artifact: wandb.Artifact, model_name: str) -> int: - for alias in artifact._attrs["aliases"]: - if ( - alias["artifactCollectionName"] == model_name - and alias["alias"][0] == "v" - and alias["alias"][1:].isnumeric() - ): - return int(alias["alias"][1:]) - return 0 - - -def upload_model_to_hub(run: Any, model_name: str, api: wandb.Api) -> int: - model_hub_path = f"{model_from_name(model_name).model_hub_base_path}/{model_name}" - entity, project = model_from_name(model_name).model_hub_base_path.split("/") - with wandb.init(entity=entity, project=project, name=model_name) as model_registry_run: - run_artifact = run.logged_artifacts()[0] - run_artifact.get_path("model.ckpt").download("/tmp") - model_registry_artifact = wandb.Artifact(model_name, type="pretrained_model") - model_registry_artifact.add_file("/tmp/model.ckpt") - model_registry_run.log_artifact(model_registry_artifact) - # run_artifact.link(model_hub_path) - # uploaded_artifact = api.artifact(f"{model_hub_path}:latest") - model_registry_run.link_artifact(model_registry_artifact, model_hub_path) - return extract_artifact_version(model_registry_artifact, model_name) - - -def compare(configA: dict, configB: dict, compare_metrics: list) -> bool: - for metric_name, mode in compare_metrics: - if metric_name not in configA or metric_name not in configB or configA[metric_name] == configB[metric_name]: - continue - # this should be correct... - return (configA[metric_name] < configB[metric_name]) == (mode == "max") - return False - - -def add_model_to_version_table( - model_kwargs: dict, version_table: pandas.DataFrame, run: Any, api: wandb.Api -) -> pandas.DataFrame: - model_kwargs["model_hub_version"] = upload_model_to_hub(run, model_kwargs["model_name"], api) - df_to_add = pandas.DataFrame([model_kwargs]) - return pandas.concat([version_table, df_to_add], ignore_index=True) - - -def update_table( - version_table: pandas.DataFrame, model_kwargs: dict, run: Any, compare_metrics: list, api: wandb.Api -) -> pandas.DataFrame: - model_comparison_keys = list(model_kwargs.keys()) - model_kwargs = convert_dtypes(model_kwargs) - version_table = add_missing_columns( - model_kwargs.keys(), # type: ignore - dict(run.summary).keys(), # type: ignore - [metric_name for metric_name, _ in compare_metrics], - ["time uploaded", "model_hub_version"], - init_values=[ - None, - None, - [-INFINITY if mode == "min" else +INFINITY for mode in [mode for _, mode in compare_metrics]], - ["", "latest"], - ], - table=version_table, - ) - - logging.info(f"extracted model config: {model_kwargs}") - model_kwargs_series = pandas.Series(model_kwargs) - existing_row = version_table[(version_table[model_comparison_keys] == model_kwargs_series).all(1)] - - model_kwargs.update(dict(run.summary)) - model_kwargs["time uploaded"] = str(datetime.datetime.now()) - - if existing_row.empty: - logging.info("adding new model configuration to version table...") - version_table = add_model_to_version_table(model_kwargs, version_table, run, api) - else: - existing_row_idx = np.where((version_table[model_comparison_keys] == model_kwargs_series).all(1))[0][0] - existing_model_kwargs = version_table.iloc[existing_row_idx].to_dict() - - # this prevents reuploading the same model by favoring the older model if the other metrics are the same - compare_metrics.append(["time uploaded", "min"]) - new_model_better = compare(existing_model_kwargs, model_kwargs, compare_metrics) - if new_model_better: - logging.info("overwriting preexisting model configuration in version table with better version...") - version_table = version_table.drop(existing_row_idx).reset_index(drop=True) - version_table = add_model_to_version_table(model_kwargs, version_table, run, api) - else: - logging.info("better/older model with same config already exists in version table, skipping...") - return version_table - - -def write_table(version_table: pandas.DataFrame, model_name: str) -> None: - version_table.to_csv(f"/tmp/versions.csv", quoting=csv.QUOTE_NONNUMERIC, index=False) - entity, project, _ = model_from_name(model_name).version_table_path.split("/") - with wandb.init(entity=entity, project=project) as run: # type: ignore - version_table_artifact = wandb.Artifact("model-tables", type="tables") - version_table_artifact.add_file("/tmp/versions.csv") - run.log_artifact(version_table_artifact) - run.link_artifact(version_table_artifact, model_from_name(model_name).version_table_path) - - -def parse_metrics(metrics: list) -> list: - compare_metrics = [] - for metric in metrics: - parts = metric.split("/") - metric_name = "/".join(parts[:-1]) - mode = parts[-1] - if metric_name is None or mode is None or mode not in ["min", "max"]: - logging.error( - f"metric cannot be parsed: {metric}. Needs to be in format /! e.g.: accuracy/max" - ) - continue - compare_metrics.append([metric_name, mode]) - return compare_metrics - - -def delete_model_version_table_in_hub() -> None: - logging.warn("DELETING MODEL VERSION TABLE IN HUB...") - input("PRESS ENTER IF YOU ARE SURE YOU WANT TO CONTINUE:") - - entity, project, _ = Model.version_table_path.split("/") - with wandb.init(entity=entity, project=project) as run: # type: ignore - table_art = wandb.Artifact("model-tables", type="tables") - with Path("versions.csv").open("w") as v: - v.write("") - table_art.add_file("versions.csv") - run.log_artifact(table_art) - run.link_artifact(table_art, Model.version_table_path) - logging.warn("MODEL VERSION TABLE DELETED!") - - -def main(args: argparse.Namespace) -> None: - if args.config is not None: - config_path = Path(args.config) - if not config_path.exists(): - raise ValueError(f"config file {str(config_path.resolve())} does not exist!") - with config_path.open() as config_file: - config = json.load(config_file) - else: - config = vars(args) - - configure_logging(logging.getLogger(), None, "info", True) - - api = wandb.Api() - filters = ( - None - if config["runs"] is None - else {"$or": [{"config.experiment_name": run_name} for run_name in config["runs"]]} - ) - entity = config["entity"] - project = config["project"] - logging.info(f"Syncing runs from: {entity}/{project}") - runs = api.runs( - path=f"{entity}/{project}", - filters=filters, - ) - - compare_metrics = parse_metrics(config["compare_metrics"]) - - version_table = download_version_table(Model.version_table_path, api, no_exception=True) - for run in tqdm(runs): - try: - if len(run.logged_artifacts()) == 0: - logging.info(f"run {run.name} has no logged artifacts, skipping...") - continue - model_kwargs = extract_model_parameters(run) - model_name = model_kwargs["model_name"] - version_table = update_table(version_table, model_kwargs, run, compare_metrics, api) - except Exception as e: - logging.info(f"run {run.name} cannot be synced with hub due to error: {e}. skipping...") - - write_table(version_table, model_name) - logging.info(f"Successfully synced {len(runs)} runs!") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser("sync script of wandb runs and the model zoo") - parser.add_argument("--config", "-c", default=None, type=str, help="path to config json file") - parser.add_argument( - "--runs", "-r", nargs="*", default=None, help="the list of runs to sync. If omitted, all runs are synced." - ) - parser.add_argument("--entity", "-e", default=None, type=str) - parser.add_argument("--project", "-p", default=None, type=str) - parser.add_argument( - "--delete-version-table", - default=False, - action="store_true", - help="deletes the remote version table on the model hub. Use with caution!", - ) - args = parser.parse_args() - - if args.delete_version_table: - delete_model_version_table_in_hub() - sys.exit(0) - main(args) From a650f5ae4c17b8734ed3ab0ac6d5ce20ace7322b Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 6 Jan 2023 17:37:07 +0100 Subject: [PATCH 199/208] made linters happy --- bitorch/models/model_hub.py | 17 ++++++++++------- mypy.ini | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 7539b5e..8c4fe55 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Dict, Any, Union +from typing import Dict, Any, Union, Tuple import os import numbers import pandas @@ -9,16 +9,18 @@ import base64 import hashlib -def md5_hash_file(path: Path): + +def _md5_hash_file(path: Path) -> Any: hash_md5 = hashlib.md5() with path.open("rb") as f: for chunk in iter(lambda: f.read(64 * 1024), b""): hash_md5.update(chunk) return hash_md5 -def digest_file(path: Union[Path, str]): - path = Path(path) - return base64.b64encode(md5_hash_file(path).digest()).decode("ascii") + +def _digest_file(path: Union[Path, str]) -> str: + return base64.b64encode(_md5_hash_file(Path(path)).digest()).decode("ascii") + def convert_dtypes(data: dict) -> dict: """converts types of the values of dict so that they can be easily compared accross @@ -59,7 +61,7 @@ def get_matching_row(version_table: pandas.DataFrame, model_kwargs: dict) -> pan return existing_row -def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict) -> str: +def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict) -> Tuple[str, str]: """finds the matching row for model_kwargs in version table and path to model artifact for given configuration Args: @@ -83,6 +85,7 @@ def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict) -> str: model_digest = matching_row["model_digest"][0] return model_url, model_digest + def load_from_hub( model_version_table_path: str, download_path: str = "bitorch_models", **model_kwargs: str ) -> torch.Tensor: @@ -103,7 +106,7 @@ def load_from_hub( model_checksum = model_path.split("/")[-1] model_local_path = Path(f"{download_path}/{model_checksum}") - if(not model_local_path.exists() or digest_file(str(model_local_path)) != model_digest): + if not model_local_path.exists() or _digest_file(str(model_local_path)) != model_digest: logging.info("downloading model...") os.system(f"wget {model_path} -O {str(model_local_path)} -q --show-progress") logging.info("Model downloaded!") diff --git a/mypy.ini b/mypy.ini index aad58c7..177e95e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ [mypy] -files = bitorch, examples, scripts +files = bitorch, examples show_error_codes = True follow_imports = silent pretty = True From 04fe51938a4e3d57dfc8dfb90bdfbef1efbdabcf Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 6 Jan 2023 17:42:26 +0100 Subject: [PATCH 200/208] added pandas dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6988b36..43065eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ torch>=1.9.0 torchvision>=0.10.0 matplotlib numpy +pandas From dd9702210ad94bf2a6d0c116de9bf6d35c5689a1 Mon Sep 17 00:00:00 2001 From: "paul.mattes" Date: Fri, 6 Jan 2023 17:49:58 +0100 Subject: [PATCH 201/208] commented faulty test case --- tests/layers/test_layer_implementation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/layers/test_layer_implementation.py b/tests/layers/test_layer_implementation.py index 10c752a..d4fb3c3 100644 --- a/tests/layers/test_layer_implementation.py +++ b/tests/layers/test_layer_implementation.py @@ -93,10 +93,10 @@ def test_default_impl(): assert isinstance(layer, LayerContainer) print(layer) # TODO: pickling is currently only possible in RAW mode - content = pickle.dumps(layer) + # content = pickle.dumps(layer) - layer_loaded = pickle.loads(content) - assert layer_loaded.val == 21 + # layer_loaded = pickle.loads(content) + # assert layer_loaded.val == 21 def test_train_impl(): From 29431a14f7a8e221e9e23e21774ec2729354e529 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 14:19:50 +0100 Subject: [PATCH 202/208] use torchvision function for file download --- bitorch/models/base.py | 1 - bitorch/models/model_hub.py | 7 +++---- requirements.txt | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bitorch/models/base.py b/bitorch/models/base.py index b60f59a..78f0d33 100644 --- a/bitorch/models/base.py +++ b/bitorch/models/base.py @@ -18,7 +18,6 @@ class Model(nn.Module): name = "" version_table_url = "https://api.wandb.ai/artifactsV2/default/hpi-deep-learning/QXJ0aWZhY3Q6MzE1MzQ1ODM1/a9bd2573417efc7fb8f562f06f3d322d" - model_hub_base_path = "hpi-deep-learning/model-registry" def __init__(self, input_shape: List[int], num_classes: int = 0) -> None: super(Model, self).__init__() diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 8c4fe55..21b611c 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -8,6 +8,7 @@ import torch import base64 import hashlib +from torchvision.datasets.utils import download_url def _md5_hash_file(path: Path) -> Any: @@ -67,7 +68,6 @@ def get_model_path(version_table: pandas.DataFrame, model_kwargs: dict) -> Tuple Args: version_table (pandas.DataFrame): version table with model configurations and corresponding model hub versions model_kwargs (dict): model configuration to search for - model_hub_base_path (str): base path that contains model hub version of models Raises: RuntimeError: thrown if no matching model can be found in version table @@ -93,7 +93,6 @@ def load_from_hub( Args: model_version_table_path (str): path to model version table on model hub - model_hub_base_path (str): base path for model hub for downloading stored models download_path (str, optional): path to store the downloaded files. Defaults to "/tmp". Returns: @@ -108,7 +107,7 @@ def load_from_hub( if not model_local_path.exists() or _digest_file(str(model_local_path)) != model_digest: logging.info("downloading model...") - os.system(f"wget {model_path} -O {str(model_local_path)} -q --show-progress") + download_url(model_path, model_local_path.parent, model_local_path.name, model_checksum) logging.info("Model downloaded!") else: logging.info(f"Using already downloaded model at {model_local_path}") @@ -151,7 +150,7 @@ def download_version_table(model_table_path: str, no_exception: bool = False) -> """ logging.info("downloading model version table from hub...") try: - os.system(f"wget {model_table_path} -O /tmp/bitorch_model_version_table.csv -q --show-progress") + download_url(model_table_path, "/tmp", "bitorch_model_version_table.csv") version_table = pandas.read_csv("/tmp/bitorch_model_version_table.csv") except Exception as e: logging.info(f"could not retrieve model version table from {model_table_path}: {e}") diff --git a/requirements.txt b/requirements.txt index 43065eb..4ebc6e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ torch>=1.9.0 -torchvision>=0.10.0 matplotlib numpy pandas From cc02c9487693056bee0189960af1b1d471ff9d63 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 14:31:04 +0100 Subject: [PATCH 203/208] adds assertion that model keys are sane --- bitorch/models/model_hub.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 21b611c..399bfe3 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -129,6 +129,10 @@ def lightning_checkpoint_to_state_dict(artifact: Dict[Any, Any]) -> Dict[Any, An Dict[Any, Any]: state dict for model """ state_dict = artifact["state_dict"] + + for key in state_dict.keys(): + assert (key.startswith("model.")), f"Unexpected malformed static dict key {key}." + # turns model._model.arg keys in state dict into _model.arg extracted_state_dict = {key[6:]: value for key, value in state_dict.items()} return extracted_state_dict From 08562ccc8e5422bffe59feb2ad2a611e733e4ab8 Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 14:53:19 +0100 Subject: [PATCH 204/208] added model hub test --- tests/models/test_model_hub.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/models/test_model_hub.py diff --git a/tests/models/test_model_hub.py b/tests/models/test_model_hub.py new file mode 100644 index 0000000..fe5feba --- /dev/null +++ b/tests/models/test_model_hub.py @@ -0,0 +1,17 @@ +from bitorch.models import ResnetE18 +import torch +import pytest +import time + +TEST_DATA = [ + (ResnetE18, {"input_shape": (1, 3, 32, 32), "num_classes": 10}), +] + + +@pytest.mark.parametrize("model, kwargs", TEST_DATA) +def test_model_hub(model, kwargs): + m = model.from_pretrained(**kwargs) + input_values = torch.randn(kwargs["input_shape"]) + + result = m(input_values) + assert result.shape == torch.Size([kwargs["input_shape"][0], kwargs["num_classes"]]) From 3ab9b8ffa25f06e75d12958601621a69a939da9f Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 15:24:10 +0100 Subject: [PATCH 205/208] made linters happy --- bitorch/models/model_hub.py | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 399bfe3..21724e2 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -1,6 +1,5 @@ from pathlib import Path from typing import Dict, Any, Union, Tuple -import os import numbers import pandas import logging diff --git a/requirements.txt b/requirements.txt index 4ebc6e0..43065eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ torch>=1.9.0 +torchvision>=0.10.0 matplotlib numpy pandas From ab305cb1f9da6de8bf094afb8ea5efbf528c298b Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 15:32:52 +0100 Subject: [PATCH 206/208] applied black --- bitorch/models/model_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitorch/models/model_hub.py b/bitorch/models/model_hub.py index 21724e2..10293d2 100644 --- a/bitorch/models/model_hub.py +++ b/bitorch/models/model_hub.py @@ -130,7 +130,7 @@ def lightning_checkpoint_to_state_dict(artifact: Dict[Any, Any]) -> Dict[Any, An state_dict = artifact["state_dict"] for key in state_dict.keys(): - assert (key.startswith("model.")), f"Unexpected malformed static dict key {key}." + assert key.startswith("model."), f"Unexpected malformed static dict key {key}." # turns model._model.arg keys in state dict into _model.arg extracted_state_dict = {key[6:]: value for key, value in state_dict.items()} From 2d1ac58d95e30835b86d06efbdd2e26a55d11e9a Mon Sep 17 00:00:00 2001 From: Snagnar Date: Fri, 13 Jan 2023 16:39:20 +0100 Subject: [PATCH 207/208] removed quicknet hack --- examples/image_classification/utils/arg_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/image_classification/utils/arg_parser.py b/examples/image_classification/utils/arg_parser.py index f422ae4..9757ea6 100644 --- a/examples/image_classification/utils/arg_parser.py +++ b/examples/image_classification/utils/arg_parser.py @@ -340,7 +340,7 @@ def add_regular_args(parser: ArgumentParser) -> None: parser.add_argument( "--model", type=str.lower, - choices=model_names() + ["quicknet"], + choices=model_names(), required=True, help="name of the model to be trained", ) From 583d59d97cbb1fe9f9745d8af714c1c7bc4feec4 Mon Sep 17 00:00:00 2001 From: Joseph Bethge Date: Fri, 13 Jan 2023 17:03:50 +0100 Subject: [PATCH 208/208] make release --- CHANGELOG.md | 2 +- version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17efad0..7dc776e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.3.0] - Unreleased +## [0.3.0] - 2023/01/13 ### Added diff --git a/version.txt b/version.txt index b9f6376..0d91a54 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.0.dev2 +0.3.0

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