diff --git a/.gitignore b/.gitignore index 085acbe9..cbe7d5a1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ model_weights/ app/models/ pip-wheel-metadata/ test_tube_exp/ +tests/tests_tt_dir/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -119,4 +120,4 @@ ENV/ .mypy_cache/ # data -mnist/ \ No newline at end of file +mnist/ diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..b85b4bc2 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,19 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation with MkDocs +mkdocs: + configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - requirements: docs/doc_requirements.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f4e08df6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: python +python: + - "3.7" +# command to install dependencies +cache: pip +install: + - pip install -e . + - pip install -r requirements.txt + - pip install -U numpy + +# keep build from timing out +dist: xenial + +# command to run tests +script: + - py.test # or py.test for Python versions 3.5 and below \ No newline at end of file diff --git a/README.md b/README.md index 6e11c297..85b27c20 100644 --- a/README.md +++ b/README.md @@ -9,31 +9,103 @@

The Keras for ML researchers using PyTorch. More control. Less boilerplate.

+

PyPI version - + PyPI version + + +

```bash -pip install pytorch-lightning +pip install pytorch-lightning ``` ## Docs **[View the docs here](https://williamfalcon.github.io/pytorch-lightning/)** ## What is it? -Keras and fast.ai are too abstract for researchers. Lightning abstracts the full training loop but gives you control in the critical points. +Lightning defers training and validation loop logic to you. It guarantees correct, modern best practices for the core training logic. ## Why do I want to use lightning? -Because you don't want to define a training loop, validation loop, gradient clipping, checkpointing, loading, -gpu training, etc... every time you start a project. Let lightning handle all of that for you! Just define your -data and what happens in the training, testing and validation loop and lightning will do the rest. +When starting a new project the last thing you want to do is recode a training loop, model loading/saving, distributed training, when to validate, etc... You're likely to spend a long time ironing out all the bugs without even getting to the core of your research. + +With lightning, you guarantee those parts of your code work so you can focus on what the meat of the research: Data and training, validation loop logic. Don't worry about multiple gpus or speeding up your code, lightning will do that for you! + +## How do I do use it? To use lightning do 2 things: -1. [Define a Trainer](https://github.com/williamFalcon/pytorch-lightning/blob/master/examples/new_project_templates/trainer_cpu_template.py). -2. [Define a LightningModel](https://github.com/williamFalcon/pytorch-lightning/blob/master/examples/new_project_templates/lightning_module_template.py). +1. [Define a LightningModel](https://williamfalcon.github.io/pytorch-lightning/LightningModule/RequiredTrainerInterface/) +```python +import pytorch_lightning as ptl +import torch +from torch.nn import functional as F +from torch.utils.data import DataLoader +from torchvision.datasets import MNIST + +class CoolModel(ptl.LightningModule): + + def __init(self): + super(CoolModel, self).__init__() + # not the best model... + self.l1 = torch.nn.Linear(28 * 28, 10) + + def forward(self, x): + return torch.relu(self.l1(x)) + + def my_loss(self, y_hat, y): + return F.cross_entropy(y_hat, y) + + def training_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'tng_loss': self.my_loss(y_hat, y)} + + def validation_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'val_loss': self.my_loss(y_hat, y)} + + def validation_end(self, outputs): + avg_loss = torch.stack([x for x in outputs['val_loss']]).mean() + return avg_loss + + def configure_optimizers(self): + return [torch.optim.Adam(self.parameters(), lr=0.02)] + + @ptl.data_loader + def tng_dataloader(self): + return DataLoader(MNIST('path/to/save', train=True), batch_size=32) + + @ptl.data_loader + def val_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) + + @ptl.data_loader + def test_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) +``` + +2. Fit with a [trainer](https://williamfalcon.github.io/pytorch-lightning/Trainer/) +```python +from pytorch_lightning import Trainer +from test_tube import Experiment + +model = CoolModel() + +# fit on 32 gpus across 4 nodes +exp = Experiment(save_dir='some/dir') +trainer = Trainer(experiment=exp, nb_gpu_nodes=4, gpus=[0,1,2,3,4,5,6,7]) + +trainer.fit(model) + +# see all experiment metrics here +# tensorboard --log_dir some/dir +``` + ## What does lightning control for me? Everything! @@ -116,8 +188,8 @@ def validation_end(self, outputs): return tqdm_dic ``` -## TensorboardX -Lightning is fully integrated with tensorboardX. +## Tensorboard +Lightning is fully integrated with tensorboard.

@@ -148,7 +220,7 @@ And run tensorboard from that dir tensorboard --logdir /some/path ``` -## Lightning automatically automates all of the following ([each is also configurable](https://williamfalcon.github.io/pytorch-lightning/Trainer/)): +## Lightning automates all of the following ([each is also configurable](https://williamfalcon.github.io/pytorch-lightning/Trainer/)): ###### Checkpointing @@ -215,19 +287,22 @@ pip install pytorch-lightning # clone lightning for the demo git clone https://github.com/williamFalcon/pytorch-lightning.git -cd examples/new_project_templates/ +cd pytorch_lightning/examples/new_project_templates/ -# run demo (on cpu) -python trainer_gpu_cluster_template.py +# all of the following demos use the SAME model to show no modification needs to be made to your code + +# train on cpu +python single_cpu_template.py + +# train on multiple-gpus +python single_gpu_node_template.py --gpus "0,1" + +# train on 32 gpus on a cluster (run on a SLURM managed cluster) +python multi_node_cluster_template.py --nb_gpu_nodes 4 --gpus '0,1,2,3,4,5,6,7' ``` -Without changing the model AT ALL, you can run the model on a single gpu, over multiple gpus, or over multiple nodes. +## Bleeding edge +If you can't wait for the next release, install the most up to date code with: ```bash -# run a grid search on two gpus -python fully_featured_trainer.py --gpus "0;1" - -# run single model on multiple gpus -python fully_featured_trainer.py --gpus "0;1" --interactive -``` - - +pip install git+https://github.com/williamFalcon/pytorch-lightning.git@master --upgrade +``` \ No newline at end of file diff --git a/coverage.svg b/coverage.svg new file mode 100644 index 00000000..6bfc8faf --- /dev/null +++ b/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 99% + 99% + + diff --git a/docs/LightningModule/RequiredTrainerInterface.md b/docs/LightningModule/RequiredTrainerInterface.md index b709c3a8..b9bb1d36 100644 --- a/docs/LightningModule/RequiredTrainerInterface.md +++ b/docs/LightningModule/RequiredTrainerInterface.md @@ -3,7 +3,7 @@ A lightning module is a strict superclass of nn.Module, it provides a standard interface for the trainer to interact with the model. -The easiest thing to do is copy [this template](../../examples/new_project_templates/lightning_module_template.py) and modify accordingly. +The easiest thing to do is copy [this template](../../pytorch_lightning/examples/new_project_templates/lightning_module_template.py) and modify accordingly. Otherwise, to Define a Lightning Module, implement the following methods: @@ -14,8 +14,6 @@ Otherwise, to Define a Lightning Module, implement the following methods: - [validation_end](RequiredTrainerInterface.md#validation_end) - [configure_optimizers](RequiredTrainerInterface.md#configure_optimizers) -- [get_save_dict](RequiredTrainerInterface.md#get_save_dict) -- [load_model_specific](RequiredTrainerInterface.md#load_model_specific) - [tng_dataloader](RequiredTrainerInterface.md#tng_dataloader) - [tng_dataloader](RequiredTrainerInterface.md#tng_dataloader) @@ -23,9 +21,63 @@ Otherwise, to Define a Lightning Module, implement the following methods: **Optional**: +- [on_save_checkpoint](RequiredTrainerInterface.md#on_save_checkpoint) +- [on_load_checkpoint](RequiredTrainerInterface.md#on_load_checkpoint) - [update_tng_log_metrics](RequiredTrainerInterface.md#update_tng_log_metrics) - [add_model_specific_args](RequiredTrainerInterface.md#add_model_specific_args) +--- +**Minimal example** +```python +import pytorch_lightning as ptl +import torch +from torch.nn import functional as F +from torch.utils.data import DataLoader +from torchvision.datasets import MNIST + +class CoolModel(ptl.LightningModule): + + def __init(self): + super(CoolModel, self).__init__() + # not the best model... + self.l1 = torch.nn.Linear(28 * 28, 10) + + def forward(self, x): + return torch.relu(self.l1(x)) + + def my_loss(self, y_hat, y): + return F.cross_entropy(y_hat, y) + + def training_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'tng_loss': self.my_loss(y_hat, y)} + + def validation_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'val_loss': self.my_loss(y_hat, y)} + + def validation_end(self, outputs): + avg_loss = torch.stack([x for x in outputs['val_loss']]).mean() + return avg_loss + + def configure_optimizers(self): + return [torch.optim.Adam(self.parameters(), lr=0.02)] + + @ptl.data_loader + def tng_dataloader(self): + return DataLoader(MNIST('path/to/save', train=True), batch_size=32) + + @ptl.data_loader + def val_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) + + @ptl.data_loader + def test_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) +``` + --- ### training_step @@ -193,34 +245,35 @@ def configure_optimizers(self): ``` --- -### get_save_dict +### on_save_checkpoint ``` {.python} -def get_save_dict(self) +def on_save_checkpoint(self, checkpoint) ``` -Called by lightning to checkpoint your model. Lightning saves current epoch, current batch nb, etc... -All you have to return is what specifically about your lightning model you want to checkpoint. +Called by lightning to checkpoint your model. Lightning saves the training state (current epoch, global_step, etc) +and also saves the model state_dict. If you want to save anything else, use this method to add your own +key-value pair. ##### Return -Dictionary - No required keys. Most of the time as described in this example. +Nothing **Example** ``` {.python} -def get_save_dict(self): - # 99% of use cases this is all you need to return - checkpoint = {'state_dict': self.state_dict()} - return checkpoint +def on_save_checkpoint(self, checkpoint): + # 99% of use cases you don't need to implement this method + checkpoint['something_cool_i_want_to_save'] = my_cool_pickable_object ``` --- -### load_model_specific +### on_load_checkpoint ``` {.python} -def load_model_specific(self, checkpoint) +def on_load_checkpoint(self, checkpoint) ``` -Called by lightning to restore your model. This is your chance to restore your model using the keys you added in get_save_dict. -Lightning will automatically restore current epoch, batch nb, etc. +Called by lightning to restore your model. Lighting auto-restores global step, epoch, etc... +It also restores the model state_dict. +If you saved something with **on_save_checkpoint** this is your chance to restore this. ##### Return Nothing @@ -228,19 +281,19 @@ Nothing **Example** ``` {.python} -def load_model_specific(self, checkpoint): - # you defined 'state_dict' in get_save_dict() - self.load_state_dict(checkpoint['state_dict']) +def on_load_checkpoint(self, checkpoint): + # 99% of the time you don't need to implement this method + self.something_cool_i_want_to_save = checkpoint['something_cool_i_want_to_save'] ``` --- ### tng_dataloader ``` {.python} -@property +@ptl.data_loader def tng_dataloader(self) ``` -Called by lightning during training loop. Define it as a property. +Called by lightning during training loop. Make sure to use the @ptl.data_loader decorator, this ensures not calling this function until the data are needed. ##### Return Pytorch DataLoader @@ -248,32 +301,26 @@ Pytorch DataLoader **Example** ``` {.python} -@property +@ptl.data_loader def tng_dataloader(self): - if self._tng_dataloader is None: - try: - transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) - dataset = MNIST(root='/path/to/mnist/', train=True, transform=transform, download=True) - loader = torch.utils.data.DataLoader( - dataset=dataset, - batch_size=self.hparams.batch_size, - shuffle=True - ) - self._tng_dataloader = loader - except Exception as e: - raise e - - return self._tng_dataloader + transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) + dataset = MNIST(root='/path/to/mnist/', train=True, transform=transform, download=True) + loader = torch.utils.data.DataLoader( + dataset=dataset, + batch_size=self.hparams.batch_size, + shuffle=True + ) + return loader ``` --- ### val_dataloader ``` {.python} -@property +@ptl.data_loader def tng_dataloader(self) ``` -Called by lightning during validation loop. Define it as a property. +Called by lightning during validation loop. Make sure to use the @ptl.data_loader decorator, this ensures not calling this function until the data are needed. ##### Return Pytorch DataLoader @@ -281,32 +328,27 @@ Pytorch DataLoader **Example** ``` {.python} -@property +@ptl.data_loader def val_dataloader(self): - if self._val_dataloader is None: - try: - transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) - dataset = MNIST(root='/path/to/mnist/', train=False, transform=transform, download=True) - loader = torch.utils.data.DataLoader( - dataset=dataset, - batch_size=self.hparams.batch_size, - shuffle=True - ) - self._val_dataloader = loader - except Exception as e: - raise e - - return self._val_dataloader + transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) + dataset = MNIST(root='/path/to/mnist/', train=False, transform=transform, download=True) + loader = torch.utils.data.DataLoader( + dataset=dataset, + batch_size=self.hparams.batch_size, + shuffle=True + ) + + return loader ``` --- ### test_dataloader ``` {.python} -@property +@ptl.data_loader def test_dataloader(self) ``` -Called by lightning during test loop. Define it as a property. +Called by lightning during test loop. Make sure to use the @ptl.data_loader decorator, this ensures not calling this function until the data are needed. ##### Return Pytorch DataLoader @@ -314,22 +356,17 @@ Pytorch DataLoader **Example** ``` {.python} -@property +@ptl.data_loader def test_dataloader(self): - if self._test_dataloader is None: - try: - transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) - dataset = MNIST(root='/path/to/mnist/', train=False, transform=transform, download=True) - loader = torch.utils.data.DataLoader( - dataset=dataset, - batch_size=self.hparams.batch_size, - shuffle=True - ) - self._test_dataloader = loader - except Exception as e: - raise e - - return self._test_dataloader + transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) + dataset = MNIST(root='/path/to/mnist/', train=False, transform=transform, download=True) + loader = torch.utils.data.DataLoader( + dataset=dataset, + batch_size=self.hparams.batch_size, + shuffle=True + ) + + return loader ``` --- diff --git a/docs/LightningModule/methods.md b/docs/LightningModule/methods.md index 9163326e..d57c6950 100644 --- a/docs/LightningModule/methods.md +++ b/docs/LightningModule/methods.md @@ -21,7 +21,8 @@ pretrained_model = MyLightningModule.load_from_metrics( map_location=None ) -# predict +# predict +pretrained_model.eval() pretrained_model.freeze() y_hat = pretrained_model(x) ``` diff --git a/docs/Trainer/Distributed training.md b/docs/Trainer/Distributed training.md index dcd8a422..a7d487b5 100644 --- a/docs/Trainer/Distributed training.md +++ b/docs/Trainer/Distributed training.md @@ -3,6 +3,26 @@ Lightning makes multi-gpu training and 16 bit training trivial. *Note:* None of the flags below require changing anything about your lightningModel definition. +--- +#### Choosing a backend +Lightning supports two backends. DataParallel and DistributedDataParallel. Both can be used for single-node multi-GPU training. +For multi-node training you must use DistributedDataParallel. + +You can toggle between each mode by setting this flag. +``` {.python} +# DEFAULT uses DataParallel +trainer = Trainer(distributed_backend='dp') + +# change to distributed data parallel +trainer = Trainer(distributed_backend='ddp') +``` + +If you request multiple nodes, the back-end will auto-switch to ddp. +We recommend you use DistributedDataparallel even for single-node multi-GPU training. It is MUCH faster than DP but *may* +have configuration issues depending on your cluster. + +For a deeper understanding of what lightning is doing, feel free to read [this guide](https://medium.com/@_willfalcon/9-tips-for-training-lightning-fast-neural-networks-in-pytorch-8e63a502f565). + --- #### 16-bit mixed precision 16 bit precision can cut your memory footprint by half. If using volta architecture GPUs it can give a dramatic training speed-up as well. @@ -37,16 +57,69 @@ Make sure you're on a GPU machine. You can set as many GPUs as you want. In this setting, the model will run on all 8 GPUs at once using DataParallel under the hood. ```python # set these flags -os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" -os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3,4,5,6,7" +# lightning sets these flags for you automatically +# no need to set yourself +# os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" +# os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3,4,5,6,7" -# DEFAULT -trainer = Trainer(gpus=[0,1,2,3,4,5,6,7]) + +# to use DataParallel (default) +trainer = Trainer(gpus=[0,1,2,3,4,5,6,7], distributed_backend='dp') + +# RECOMMENDED use DistributedDataParallel +trainer = Trainer(gpus=[0,1,2,3,4,5,6,7], distributed_backend='ddp') ``` --- #### Multi-node -COMING SOON. +Multi-node training is easily done by specifying these flags. +```python +# train on 12*8 GPUs +trainer = Trainer(gpus=[0,1,2,3,4,5,6,7], nb_gpu_nodes=12) +``` + +In addition, make sure to set up your SLURM job correctly via the [SlurmClusterObject](https://williamfalcon.github.io/test-tube/hpc/SlurmCluster/). In particular, specify the number of tasks per node correctly. + +```python +cluster = SlurmCluster( + hyperparam_optimizer=test_tube.HyperOptArgumentParser(), + log_path='/some/path/to/save', +) + +# OPTIONAL FLAGS WHICH MAY BE CLUSTER DEPENDENT +# which interface your nodes use for communication +cluster.add_command('export NCCL_SOCKET_IFNAME=^docker0,lo') + +# see output of the NCCL connection process +# NCCL is how the nodes talk to each other +cluster.add_command('export NCCL_DEBUG=INFO') + +# setting a master port here is a good idea. +cluster.add_command(f'export MASTER_PORT={PORT}') + +# good to load the latest NCCL version +cluster.load_modules(['NCCL/2.4.7-1-cuda.10.0']) + +# configure cluster +cluster.per_experiment_nb_nodes = 12 +cluster.per_experiment_nb_gpus = 8 + +cluster.add_slurm_cmd(cmd='ntasks-per-node', value=8, comment='1 task per gpu') +``` + +Finally, make sure to add a distributed sampler to your dataset. The distributed sampler copies a +portion of your dataset onto each GPU. (World_size = gpus_per_node * nb_nodes). + +```python +# ie: this: +dataset = myDataset() +dataloader = Dataloader(dataset) + +# becomes: +dataset = myDataset() +dist_sampler = torch.utils.data.distributed.DistributedSampler(dataset) +dataloader = Dataloader(dataset, sampler=dist_sampler) +``` --- #### Self-balancing architecture diff --git a/docs/Trainer/Training Loop.md b/docs/Trainer/Training Loop.md index 9b05fd58..2be8da9e 100644 --- a/docs/Trainer/Training Loop.md +++ b/docs/Trainer/Training Loop.md @@ -19,7 +19,7 @@ Cut the learning rate by 10 at every epoch listed in this list. trainer = Trainer(lr_scheduler_milestones=None) # cut LR by 10 at 100, 200, and 300 epochs -trainer = Trainer(lr_scheduler_milestones=[100, 200, 300]) +trainer = Trainer(lr_scheduler_milestones='100, 200, 300') ``` --- diff --git a/docs/doc_requirements.txt b/docs/doc_requirements.txt new file mode 100644 index 00000000..d52ea0ac --- /dev/null +++ b/docs/doc_requirements.txt @@ -0,0 +1 @@ +mkdocs-material==4.4.0 diff --git a/docs/index.md b/docs/index.md index 01e3b292..0e25fa79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,11 @@ ###### New project Quick Start To start a new project define these two files. -1. [Define a LightningModule](/LightningModule/RequiredTrainerInterface/#template-model-definition) -2. Pick a trainer - - [Basic CPU Trainer](https://github.com/williamFalcon/pytorch-lightning/blob/master/examples/new_project_templates/trainer_cpu_template.py) - - [GPU cluster Trainer](https://github.com/williamFalcon/pytorch-lightning/blob/master/examples/new_project_templates/trainer_gpu_cluster_template.py) +1. [Define a LightningModule](/pytorch-lightning/LightningModule/RequiredTrainerInterface/) +2. [Define a trainer](https://williamfalcon.github.io/pytorch-lightning/Trainer/) + - [Basic CPU Trainer Template](https://github.com/williamFalcon/pytorch-lightning/blob/master/pytorch_lightning/examples/new_project_templates/single_cpu_template.py) + - [Multi-GPU Trainer Template](https://github.com/williamFalcon/pytorch-lightning/blob/master/pytorch_lightning/examples/new_project_templates/single_gpu_node_template.py) + - [GPU cluster Trainer Template](https://github.com/williamFalcon/pytorch-lightning/blob/master/pytorch_lightning/examples/new_project_templates/multi_node_cluster_template.py) ###### Docs shortcuts - [LightningModule](LightningModule/RequiredTrainerInterface/) diff --git a/examples/new_project_templates/__init__.py b/examples/new_project_templates/__init__.py deleted file mode 100644 index bc8ec6d7..00000000 --- a/examples/new_project_templates/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .lightning_module_template import LightningTemplateModel \ No newline at end of file diff --git a/pytorch_lightning/__init__.py b/pytorch_lightning/__init__.py index 5893eb4a..e7a03ba8 100644 --- a/pytorch_lightning/__init__.py +++ b/pytorch_lightning/__init__.py @@ -1,2 +1,3 @@ from .models import Trainer -from .root_module.root_module import LightningModule \ No newline at end of file +from .root_module.root_module import LightningModule +from .root_module.decorators import data_loader \ No newline at end of file diff --git a/pytorch_lightning/examples/__init__.py b/pytorch_lightning/examples/__init__.py new file mode 100644 index 00000000..6743d7f9 --- /dev/null +++ b/pytorch_lightning/examples/__init__.py @@ -0,0 +1 @@ +from .new_project_templates.lightning_module_template import LightningTemplateModel \ No newline at end of file diff --git a/pytorch_lightning/examples/new_project_templates/__init__.py b/pytorch_lightning/examples/new_project_templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/new_project_templates/lightning_module_template.py b/pytorch_lightning/examples/new_project_templates/lightning_module_template.py similarity index 79% rename from examples/new_project_templates/lightning_module_template.py rename to pytorch_lightning/examples/new_project_templates/lightning_module_template.py index 5245e2cd..608e534e 100644 --- a/examples/new_project_templates/lightning_module_template.py +++ b/pytorch_lightning/examples/new_project_templates/lightning_module_template.py @@ -10,6 +10,7 @@ from torch import optim from torch.utils.data import DataLoader from torch.utils.data.distributed import DistributedSampler +import pytorch_lightning as ptl from pytorch_lightning.root_module.root_module import LightningModule @@ -24,10 +25,14 @@ class LightningTemplateModel(LightningModule): :param hparams: """ # init superclass - super(LightningTemplateModel, self).__init__(hparams) + super(LightningTemplateModel, self).__init__() + self.hparams = hparams self.batch_size = hparams.batch_size + # if you specify an example input, the summary will show input/output for each layer + self.example_input_array = torch.rand(5, 28 * 28) + # build model self.__build_model() @@ -78,15 +83,21 @@ class LightningTemplateModel(LightningModule): # forward pass x, y = data_batch x = x.view(x.size(0), -1) + y_hat = self.forward(x) # calculate loss loss_val = self.loss(y, y_hat) + # in DP mode (default) make sure if result is scalar, there's another dim in the beginning + if self.trainer.use_dp: + loss_val = loss_val.unsqueeze(0) + output = OrderedDict({ - 'loss': loss_val, - 'tqdm_metrics': {} + 'loss': loss_val }) + + # can also return just a scalar instead of a dict (return loss_val) return output def validation_step(self, data_batch, batch_i): @@ -104,11 +115,22 @@ class LightningTemplateModel(LightningModule): # acc labels_hat = torch.argmax(y_hat, dim=1) val_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + val_acc = torch.tensor(val_acc) + + if self.on_gpu: + val_acc = val_acc.cuda(loss_val.device.index) + + # in DP mode (default) make sure if result is scalar, there's another dim in the beginning + if self.trainer.use_dp: + loss_val = loss_val.unsqueeze(0) + val_acc = val_acc.unsqueeze(0) output = OrderedDict({ 'val_loss': loss_val, - 'val_acc': torch.tensor(val_acc), + 'val_acc': val_acc, }) + + # can also return just a scalar instead of a dict (return loss_val) return output def validation_end(self, outputs): @@ -117,6 +139,10 @@ class LightningTemplateModel(LightningModule): :param outputs: list of individual outputs of each validation step :return: """ + # if returned a scalar from validation_step, outputs is a list of tensor scalars + # we return just the average in this case (if we want) + # return torch.stack(outputs).mean() + val_loss_mean = 0 val_acc_mean = 0 for output in outputs: @@ -128,20 +154,6 @@ class LightningTemplateModel(LightningModule): tqdm_dic = {'val_loss': val_loss_mean.item(), 'val_acc': val_acc_mean.item()} return tqdm_dic - def update_tng_log_metrics(self, logs): - return logs - - # --------------------- - # MODEL SAVING - # --------------------- - def get_save_dict(self): - checkpoint = {'state_dict': self.state_dict()} - return checkpoint - - def load_model_specific(self, checkpoint): - self.load_state_dict(checkpoint['state_dict']) - pass - # --------------------- # TRAINING SETUP # --------------------- @@ -179,38 +191,23 @@ class LightningTemplateModel(LightningModule): return loader - @property + @ptl.data_loader def tng_dataloader(self): - if self._tng_dataloader is None: - try: - self._tng_dataloader = self.__dataloader(train=True) - except Exception as e: - print(e) - raise e - return self._tng_dataloader + print('tng data loader called') + return self.__dataloader(train=True) - @property + @ptl.data_loader def val_dataloader(self): - if self._val_dataloader is None: - try: - self._val_dataloader = self.__dataloader(train=False) - except Exception as e: - print(e) - raise e - return self._val_dataloader + print('val data loader called') + return self.__dataloader(train=False) - @property + @ptl.data_loader def test_dataloader(self): - if self._test_dataloader is None: - try: - self._test_dataloader = self.__dataloader(train=False) - except Exception as e: - print(e) - raise e - return self._test_dataloader + print('test data loader called') + return self.__dataloader(train=False) @staticmethod - def add_model_specific_args(parent_parser, root_dir): + def add_model_specific_args(parent_parser, root_dir): # pragma: no cover """ Parameters you define here will be available to your model through self.hparams :param parent_parser: diff --git a/examples/new_project_templates/multi_node_cluster_template.py b/pytorch_lightning/examples/new_project_templates/multi_node_cluster_template.py similarity index 100% rename from examples/new_project_templates/multi_node_cluster_template.py rename to pytorch_lightning/examples/new_project_templates/multi_node_cluster_template.py diff --git a/examples/new_project_templates/single_cpu_template.py b/pytorch_lightning/examples/new_project_templates/single_cpu_template.py similarity index 100% rename from examples/new_project_templates/single_cpu_template.py rename to pytorch_lightning/examples/new_project_templates/single_cpu_template.py diff --git a/examples/new_project_templates/single_gpu_node_16bit_template.py b/pytorch_lightning/examples/new_project_templates/single_gpu_node_16bit_template.py similarity index 100% rename from examples/new_project_templates/single_gpu_node_16bit_template.py rename to pytorch_lightning/examples/new_project_templates/single_gpu_node_16bit_template.py diff --git a/examples/new_project_templates/single_gpu_node_template.py b/pytorch_lightning/examples/new_project_templates/single_gpu_node_dp_template.py similarity index 100% rename from examples/new_project_templates/single_gpu_node_template.py rename to pytorch_lightning/examples/new_project_templates/single_gpu_node_dp_template.py diff --git a/pytorch_lightning/examples/new_project_templates/single_gpu_node_template.py b/pytorch_lightning/examples/new_project_templates/single_gpu_node_template.py new file mode 100644 index 00000000..34dc4441 --- /dev/null +++ b/pytorch_lightning/examples/new_project_templates/single_gpu_node_template.py @@ -0,0 +1,112 @@ +""" +Runs a model on a single node across N-gpus. +""" +import os +import sys +import numpy as np +from time import sleep +import torch + +from test_tube import HyperOptArgumentParser, Experiment, SlurmCluster +from pytorch_lightning.models.trainer import Trainer +from pytorch_lightning.utils.arg_parse import add_default_args + +from pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint + +SEED = 2334 +torch.manual_seed(SEED) +np.random.seed(SEED) + +from lightning_module_template import LightningTemplateModel + + +def main(hparams): + """ + Main training routine specific for this project + :param hparams: + :return: + """ + # ------------------------ + # 1 INIT LIGHTNING MODEL + # ------------------------ + print('loading model...') + model = LightningTemplateModel(hparams) + print('model built') + + # ------------------------ + # 2 INIT TEST TUBE EXP + # ------------------------ + + # init experiment + exp = Experiment( + name=hyperparams.experiment_name, + save_dir=hyperparams.test_tube_save_path, + autosave=False, + description='test demo' + ) + + exp.argparse(hparams) + exp.save() + + # ------------------------ + # 3 DEFINE CALLBACKS + # ------------------------ + model_save_path = '{}/{}/{}'.format(hparams.model_save_path, exp.name, exp.version) + early_stop = EarlyStopping( + monitor='val_acc', + patience=3, + verbose=True, + mode='max' + ) + + checkpoint = ModelCheckpoint( + filepath=model_save_path, + save_best_only=True, + verbose=True, + monitor='val_loss', + mode='min' + ) + + # ------------------------ + # 4 INIT TRAINER + # ------------------------ + trainer = Trainer( + experiment=exp, + checkpoint_callback=checkpoint, + early_stop_callback=early_stop, + gpus=hparams.gpus, + ) + + # ------------------------ + # 5 START TRAINING + # ------------------------ + trainer.fit(model) + + +if __name__ == '__main__': + + # dirs + root_dir = os.path.dirname(os.path.realpath(__file__)) + demo_log_dir = os.path.join(root_dir, 'pt_lightning_demo_logs') + checkpoint_dir = os.path.join(demo_log_dir, 'model_weights') + test_tube_dir = os.path.join(demo_log_dir, 'test_tube_data') + + # although we user hyperOptParser, we are using it only as argparse right now + parent_parser = HyperOptArgumentParser(strategy='grid_search', add_help=False) + + # gpu args + parent_parser.add_argument('--gpus', type=str, default='-1', help='how many gpus to use in the node. -1 uses all the gpus on the node') + parent_parser.add_argument('--test_tube_save_path', type=str, default=test_tube_dir, help='where to save logs') + parent_parser.add_argument('--model_save_path', type=str, default=checkpoint_dir, help='where to save model') + parent_parser.add_argument('--experiment_name', type=str, default='pt_lightning_exp_a', help='test tube exp name') + + # allow model to overwrite or extend args + parser = LightningTemplateModel.add_model_specific_args(parent_parser, root_dir) + hyperparams = parser.parse_args() + + # --------------------- + # RUN TRAINING + # --------------------- + # run on HPC cluster + print(f'RUNNING INTERACTIVE MODE ON GPUS. gpu ids: {hyperparams.gpus}') + main(hyperparams) diff --git a/pytorch_lightning/examples/new_project_templates/single_gpu_template.py b/pytorch_lightning/examples/new_project_templates/single_gpu_template.py new file mode 100644 index 00000000..66714230 --- /dev/null +++ b/pytorch_lightning/examples/new_project_templates/single_gpu_template.py @@ -0,0 +1,112 @@ +""" +Runs a model on a single node across N-gpus. +""" +import os +import sys +import numpy as np +from time import sleep +import torch + +from test_tube import HyperOptArgumentParser, Experiment, SlurmCluster +from pytorch_lightning.models.trainer import Trainer +from pytorch_lightning.utils.arg_parse import add_default_args + +from pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint + +SEED = 2334 +torch.manual_seed(SEED) +np.random.seed(SEED) + +from lightning_module_template import LightningTemplateModel + + +def main(hparams): + """ + Main training routine specific for this project + :param hparams: + :return: + """ + # ------------------------ + # 1 INIT LIGHTNING MODEL + # ------------------------ + print('loading model...') + model = LightningTemplateModel(hparams) + print('model built') + + # ------------------------ + # 2 INIT TEST TUBE EXP + # ------------------------ + + # init experiment + exp = Experiment( + name=hyperparams.experiment_name, + save_dir=hyperparams.test_tube_save_path, + autosave=False, + description='test demo' + ) + + exp.argparse(hparams) + exp.save() + + # ------------------------ + # 3 DEFINE CALLBACKS + # ------------------------ + model_save_path = '{}/{}/{}'.format(hparams.model_save_path, exp.name, exp.version) + early_stop = EarlyStopping( + monitor='val_acc', + patience=3, + verbose=True, + mode='max' + ) + + checkpoint = ModelCheckpoint( + filepath=model_save_path, + save_best_only=True, + verbose=True, + monitor='val_loss', + mode='min' + ) + + # ------------------------ + # 4 INIT TRAINER + # ------------------------ + trainer = Trainer( + experiment=exp, + checkpoint_callback=checkpoint, + early_stop_callback=early_stop, + gpus=hparams.gpus, + ) + + # ------------------------ + # 5 START TRAINING + # ------------------------ + trainer.fit(model) + + +if __name__ == '__main__': + + # dirs + root_dir = os.path.dirname(os.path.realpath(__file__)) + demo_log_dir = os.path.join(root_dir, 'pt_lightning_demo_logs') + checkpoint_dir = os.path.join(demo_log_dir, 'model_weights') + test_tube_dir = os.path.join(demo_log_dir, 'test_tube_data') + + # although we user hyperOptParser, we are using it only as argparse right now + parent_parser = HyperOptArgumentParser(strategy='grid_search', add_help=False) + + # gpu args + parent_parser.add_argument('--gpus', type=str, default='0', help='how many gpus to use in the node. -1 uses all the gpus on the node') + parent_parser.add_argument('--test_tube_save_path', type=str, default=test_tube_dir, help='where to save logs') + parent_parser.add_argument('--model_save_path', type=str, default=checkpoint_dir, help='where to save model') + parent_parser.add_argument('--experiment_name', type=str, default='pt_lightning_exp_a', help='test tube exp name') + + # allow model to overwrite or extend args + parser = LightningTemplateModel.add_model_specific_args(parent_parser, root_dir) + hyperparams = parser.parse_args() + + # --------------------- + # RUN TRAINING + # --------------------- + # run on HPC cluster + print(f'RUNNING INTERACTIVE MODE ON GPUS. gpu ids: {hyperparams.gpus}') + main(hyperparams) diff --git a/examples/new_project_templates/trainer_cpu_template.py b/pytorch_lightning/examples/new_project_templates/trainer_cpu_template.py similarity index 100% rename from examples/new_project_templates/trainer_cpu_template.py rename to pytorch_lightning/examples/new_project_templates/trainer_cpu_template.py diff --git a/pytorch_lightning/models/trainer.py b/pytorch_lightning/models/trainer.py new file mode 100644 index 00000000..d686e39f --- /dev/null +++ b/pytorch_lightning/models/trainer.py @@ -0,0 +1,891 @@ +""" +The trainer handles all the logic for running a val loop, training loop, distributing, etc... +""" +import subprocess +import traceback +import warnings +import os +import pdb +import re + +import torch +from torch.utils.data.distributed import DistributedSampler +from torch.optim.lr_scheduler import MultiStepLR +import torch.multiprocessing as mp +import torch.distributed as dist +import numpy as np +import tqdm + +from pytorch_lightning.root_module.memory import get_gpu_memory_map +from pytorch_lightning.root_module.model_saving import TrainerIO +from pytorch_lightning.pt_overrides.override_data_parallel import LightningDistributedDataParallel, LightningDataParallel +from pytorch_lightning.utils.debugging import MisconfigurationException + +try: + from apex import amp + APEX_AVAILABLE = True +except Exception: + APEX_AVAILABLE = False + + +def reduce_distributed_output(output, nb_gpus): + if nb_gpus <= 1: + return output + + # when using DP, we get one output per gpu + # average outputs and return + if type(output) is torch.Tensor: + return output.mean() + + for k, v in output.items(): + # recurse on nested dics + if isinstance(output[k], dict): + output[k] = reduce_distributed_output(output[k], nb_gpus) + + # reduce only metrics that have the same nb of gpus + elif output[k].size(0) == nb_gpus: + reduced = torch.mean(output[k]) + output[k] = reduced + return output + + +class Trainer(TrainerIO): + + def __init__(self, + experiment, + early_stop_callback=None, + checkpoint_callback=None, + gradient_clip=0, + cluster=None, + process_position=0, + current_gpu_name=0, + nb_gpu_nodes=1, + gpus=None, + progress_bar=True, + overfit_pct=0.0, + track_grad_norm=-1, + check_val_every_n_epoch=1, + fast_dev_run=False, + accumulate_grad_batches=1, + max_nb_epochs=1000, min_nb_epochs=1, + train_percent_check=1.0, val_percent_check=1.0, test_percent_check=1.0, + val_check_interval=0.95, + log_save_interval=100, add_log_row_interval=10, + lr_scheduler_milestones=None, + distributed_backend='dp', + use_amp=False, + print_nan_grads=False, + print_weights_summary=True, + amp_level='O2', + nb_sanity_val_steps=5): + + """ + + :param experiment: Test-tube experiment + :param early_stop_callback: from pytorch_lightning import EarlyStopping + :param checkpoint_callback: from pytorch_lightning import Checkpoint + :param gradient_clip: + :param cluster: + :param process_position: + :param current_gpu_name: + :param nb_gpu_nodes: + :param gpus: + :param progress_bar: + :param overfit_pct: + :param track_grad_norm: + :param check_val_every_n_epoch: + :param fast_dev_run: + :param accumulate_grad_batches: + :param max_nb_epochs: + :param min_nb_epochs: + :param train_percent_check: + :param val_percent_check: + :param test_percent_check: + :param val_check_interval: + :param log_save_interval: + :param add_log_row_interval: + :param lr_scheduler_milestones: + :param distributed_backend: 'np' to use DistributedParallel, 'ddp' to use DistributedDataParallel + :param use_amp: + :param print_nan_grads: + :param print_weights_summary: + :param amp_level: + :param nb_sanity_val_steps: + """ + + # Transfer params + + self.nb_gpu_nodes = nb_gpu_nodes + self.gradient_clip = gradient_clip + self.check_val_every_n_epoch = check_val_every_n_epoch + self.enable_early_stop = early_stop_callback is not None + self.track_grad_norm = track_grad_norm + self.fast_dev_run = fast_dev_run + self.on_gpu = gpus is not None and torch.cuda.is_available() + self.progress_bar = progress_bar + self.experiment = experiment + self.exp_save_path = experiment.get_data_path(experiment.name, experiment.version) + self.cluster = cluster + self.process_position = process_position + self.current_gpu_name = current_gpu_name + self.print_weights_summary = print_weights_summary + self.checkpoint_callback = checkpoint_callback + + if self.checkpoint_callback is not None: + self.checkpoint_callback.save_function = self.save_checkpoint + + self.early_stop = early_stop_callback + self.model = None + self.max_nb_epochs = max_nb_epochs + self.accumulate_grad_batches = accumulate_grad_batches + self.early_stop_callback = early_stop_callback + self.min_nb_epochs = min_nb_epochs + self.nb_sanity_val_steps = nb_sanity_val_steps + self.lr_scheduler_milestones = [] if lr_scheduler_milestones is None else [int(x.strip()) for x in lr_scheduler_milestones.split(',')] + self.lr_schedulers = [] + self.amp_level = amp_level + self.print_nan_grads = print_nan_grads + self.data_parallel_device_ids = None + self.world_size = 1 + self.node_rank = 0 + self.use_ddp = False + self.use_dp = False + + # training bookeeping + self.total_batch_nb = 0 + self.running_loss = [] + self.avg_loss = 0 + self.batch_nb = 0 + self.tqdm_metrics = {} + self.nb_val_batches = None + self.nb_tng_batches = None + self.nb_test_batches = None + + # gpus come in as a string. + # if gpus = -1 then use all available devices + # otherwise, split the string using commas + if gpus is not None: + if type(gpus) is list: + self.data_parallel_device_ids = gpus + elif type(gpus) is str: + if gpus == '-1': + self.data_parallel_device_ids = list(range(0, torch.cuda.device_count())) + else: + self.data_parallel_device_ids = [int(x.strip()) for x in gpus.split(',')] + else: + raise Exception('gpus has to be a string or list of ids') + + # set the correct cuda visible devices (using pci order) + os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" + os.environ["CUDA_VISIBLE_DEVICES"] = ','.join([str(x) for x in self.data_parallel_device_ids]) + print(f'VISIBLE GPUS: {os.environ["CUDA_VISIBLE_DEVICES"]}') + + # make DP and DDP mutually exclusive + # single GPU will also use DP with devices=[0] + have_gpus = self.data_parallel_device_ids is not None and len(self.data_parallel_device_ids) > 0 + if have_gpus: + self.use_dp = distributed_backend == 'dp' + self.use_ddp = distributed_backend == 'ddp' + + # use ddp automatically if nb_gpu_nodes > 1 + if nb_gpu_nodes > 1 and self.use_dp: # pragma: no cover + self.use_ddp = True + self.use_dp = False + w = 'DataParallel does not support nb_gpu_nodes > 1. ' \ + 'Switching to DistributedDataParallel for you. ' \ + 'To silence this warning set distributed_backend=ddp' + warnings.warn(w) + + # extract SLURM flag vars + # whenever we have the correct number of tasks, we let slurm manage processes + # otherwise we launch the required number of processes + if self.use_ddp: + self.nb_requested_gpus = len(self.data_parallel_device_ids) * self.nb_gpu_nodes + self.nb_slurm_tasks = 0 + try: + self.nb_slurm_tasks = int(os.environ['SLURM_NTASKS']) + self.is_slurm_managing_tasks = self.nb_slurm_tasks == self.nb_requested_gpus + except Exception as e: + # likely not on slurm, so set the slurm managed flag to false + self.is_slurm_managing_tasks = False + + # process info + self.proc_rank = 0 + + # training state + self.optimizers = None + self.prog_bar = None + self.global_step = 0 + self.current_epoch = 0 + self.total_batches = 0 + + # logging + self.log_save_interval = log_save_interval + self.val_check_interval = val_check_interval + self.add_log_row_interval = add_log_row_interval + + # dataloaders + self.tng_dataloader = None + self.test_dataloader = None + self.val_dataloader = None + + # how much of the data to use + self.__determine_data_use_amount(train_percent_check, val_percent_check, test_percent_check, overfit_pct) + print('gpu available: {}, used: {}'.format(torch.cuda.is_available(), self.on_gpu)) + + # 16 bit mixed precision training using apex + self.use_amp = use_amp and APEX_AVAILABLE + if self.use_amp: + print('using 16bit precision') + + if use_amp and not APEX_AVAILABLE: # pragma: no cover + msg = ''' + You set use_amp=True but do not have apex installed. + Install apex first using this guide and rerun with use_amp=True: + https://github.com/NVIDIA/apex#linux + + this run will NOT use 16 bit precision + ''' + raise ModuleNotFoundError(msg) + + @property + def data_parallel(self): + return self.use_dp or self.use_ddp + + def __determine_data_use_amount(self, train_percent_check, val_percent_check, test_percent_check, overfit_pct): + """ + Use less data for debugging purposes + """ + self.train_percent_check = train_percent_check + self.val_percent_check = val_percent_check + self.test_percent_check = test_percent_check + if overfit_pct > 0: + self.train_percent_check = overfit_pct + self.val_percent_check = overfit_pct + self.test_percent_check = overfit_pct + + def __get_model(self): + return self.model.module if self.data_parallel else self.model + + def __is_function_implemented(self, f_name): + model = self.__get_model() + f_op = getattr(model, f_name, None) + return callable(f_op) + + @property + def __tng_tqdm_dic(self): + # ForkedPdb().set_trace() + tqdm_dic = { + 'tng_loss': '{0:.3f}'.format(self.avg_loss), + 'v_nb': '{}'.format(self.experiment.version), + 'epoch': '{}'.format(self.current_epoch), + 'batch_nb':'{}'.format(self.batch_nb), + } + tqdm_dic.update(self.tqdm_metrics) + + if self.on_gpu: + tqdm_dic['gpu'] = '{}'.format(self.current_gpu_name) + + return tqdm_dic + + @property + def tng_tqdm_dic(self): + """ + Read-only for tqdm metrics + :return: + """ + return self.__tng_tqdm_dic + + def __layout_bookeeping(self): + + # determine number of training batches + self.nb_tng_batches = len(self.tng_dataloader) + self.nb_tng_batches = int(self.nb_tng_batches * self.train_percent_check) + + # determine number of validation batches + self.nb_val_batches = len(self.val_dataloader) + self.nb_val_batches = int(self.nb_val_batches * self.val_percent_check) + self.nb_val_batches = max(1, self.nb_val_batches) + self.nb_val_batches = self.nb_val_batches + + # determine number of test batches + self.nb_test_batches = len(self.test_dataloader) + self.nb_test_batches = int(self.nb_test_batches * self.test_percent_check) + + # determine when to check validation + self.val_check_batch = int(self.nb_tng_batches * self.val_check_interval) + + def __add_tqdm_metrics(self, metrics): + for k, v in metrics.items(): + if type(v) is torch.Tensor: + v = v.item() + + self.tqdm_metrics[k] = v + + def validate(self, model, dataloader, max_batches): + """ + Run validation code + :param model: PT model + :param dataloader: PT dataloader + :param max_batches: Scalar + :return: + """ + # enable eval mode + model.zero_grad() + model.eval() + + # disable gradients to save memory + torch.set_grad_enabled(False) + + # bookkeeping + outputs = [] + + # run training + for batch_i, data_batch in enumerate(dataloader): + + if data_batch is None: # pragma: no cover + continue + + # stop short when on fast dev run + if max_batches is not None and batch_i >= max_batches: + break + + # ----------------- + # RUN VALIDATION STEP + # ----------------- + if self.use_ddp: + output = model(data_batch, batch_i) + elif self.use_dp: + output = model(data_batch, batch_i) + output = reduce_distributed_output(output, len(self.data_parallel_device_ids)) + + else: + output = model.validation_step(data_batch, batch_i) + + outputs.append(output) + + # batch done + if self.progress_bar and self.prog_bar is not None: + self.prog_bar.update(1) + + # give model a chance to do something with the outputs + if self.data_parallel: + val_results = model.module.validation_end(outputs) + else: + val_results = model.validation_end(outputs) + + # enable train mode again + model.train() + + # enable gradients to save memory + torch.set_grad_enabled(True) + + return val_results + + def get_dataloaders(self, model): + """ + Dataloaders are provided by the model + :param model: + :return: + """ + self.tng_dataloader = model.tng_dataloader + self.test_dataloader = model.test_dataloader + self.val_dataloader = model.val_dataloader + + if self.use_ddp and not isinstance(self.tng_dataloader.sampler, DistributedSampler): + msg = ''' + when using multiple gpus and multiple nodes you must pass a DistributedSampler to DataLoader(sampler). + + ie: this: + dataset = myDataset() + dataloader = Dataloader(dataset) + + becomes: + dataset = myDataset() + dist_sampler = torch.utils.data.distributed.DistributedSampler(dataset) + dataloader = Dataloader(dataset, sampler=dist_sampler) + ''' + raise MisconfigurationException(msg) + + # ----------------------------- + # MODEL TRAINING + # ----------------------------- + def fit(self, model): + + # when using multi-node or DDP within a node start each module in a separate process + if self.use_ddp: + # must copy only the meta of the exp so it survives pickle/unpickle when going to new process + self.experiment = self.experiment.get_meta_copy() + + if self.is_slurm_managing_tasks: + task = int(os.environ['SLURM_LOCALID']) + self.ddp_train(task, model) + else: + msg = f""" + You requested {self.nb_requested_gpus} GPUs but launched {self.nb_slurm_tasks} slurm tasks. + We will launch {self.nb_requested_gpus} processes for you. + We recommend you let slurm manage the processes by setting: --ntasks-per-node={self.nb_requested_gpus} + If you're not using SLURM, ignore this message! + """ + warnings.warn(msg) + mp.spawn(self.ddp_train, nprocs=len(self.data_parallel_device_ids), args=(model, )) + + # 1 gpu or dp option triggers training using DP module + # easier to avoid NCCL issues + elif self.use_dp: + self.__dp_train(model) + + # ON CPU + else: + # run through amp wrapper + if self.use_amp: + raise MisconfigurationException('amp + cpu is not supported. Please use a GPU option') + + # CHOOSE OPTIMIZER + # filter out the weights that were done on gpu so we can load on good old cpus + self.optimizers = model.configure_optimizers() + + self.__run_pretrain_routine(model) + + # return 1 when finished + # used for testing or when we need to know that training succeeded + return 1 + + def __dp_train(self, model): + + # CHOOSE OPTIMIZER + # filter out the weights that were done on gpu so we can load on good old cpus + self.optimizers = model.configure_optimizers() + + model.cuda(self.data_parallel_device_ids[0]) + + # check for this bug (amp + dp + !01 doesn't work) + # https://github.com/NVIDIA/apex/issues/227 + if self.use_dp and self.use_amp: + m = f'amp level {self.amp_level} with DataParallel is not supported. ' \ + f'See this note from NVIDIA for more info: https://github.com/NVIDIA/apex/issues/227. ' \ + f'We recommend you switch to ddp if you want to use amp' + raise MisconfigurationException(m) + + model = LightningDataParallel(model, device_ids=self.data_parallel_device_ids) + + self.__run_pretrain_routine(model) + + def ddp_train(self, gpu_nb, model): + """ + Entry point into a DP thread + :param gpu_nb: + :param model: + :param cluster_obj: + :return: + """ + # node rank using relative slurm id + # otherwise default to node rank 0 + try: + node_id = os.environ['SLURM_NODEID'] + self.node_rank = int(node_id) + except Exception as e: + self.node_rank = 0 + + # recover original exp before went into process + # init in write mode only on proc 0 + self.experiment.debug = self.proc_rank > 0 + self.experiment = self.experiment.get_non_ddp_exp() + + # show progbar only on prog_rank 0 + self.prog_bar = self.prog_bar and self.node_rank == 0 and gpu_nb == 0 + + # determine which process we are and world size + self.proc_rank = self.node_rank * len(self.data_parallel_device_ids) + gpu_nb + self.world_size = self.nb_gpu_nodes * len(self.data_parallel_device_ids) + + # let the exp know the rank to avoid overwriting logs + self.experiment.rank = self.proc_rank + + # set up server using proc 0's ip address + # try to init for 20 times at max in case ports are taken + # where to store ip_table + self.__init_tcp_connection() + + # CHOOSE OPTIMIZER + # filter out the weights that were done on gpu so we can load on good old cpus + self.optimizers = model.configure_optimizers() + + # MODEL + # copy model to each gpu + torch.cuda.set_device(gpu_nb) + model.cuda(gpu_nb) + + # AMP + # run through amp wrapper before going to distributed DP + if self.use_amp: + # An example + model, optimizers = amp.initialize( + model, self.optimizers, opt_level=self.amp_level, + ) + self.optimizers = optimizers + + model = LightningDistributedDataParallel(model, device_ids=[gpu_nb], find_unused_parameters=True) + + # continue training routine + self.__run_pretrain_routine(model) + + def __init_tcp_connection(self): + """ + Connect all procs in the world using the env:// init + Use the first node as the root address + :param port: + :param tries: + :return: + """ + # sets the appropriate port + try: + port = os.environ['MASTER_PORT'] + except Exception as e: + port = 12910 + os.environ['MASTER_PORT'] = f'{port}' + + # figure out the root node addr + try: + root_node = os.environ['SLURM_NODELIST'].split(' ')[0] + except Exception as e: + root_node = '127.0.0.2' + + root_node = self.resolve_root_node_address(root_node) + os.environ['MASTER_ADDR'] = root_node + + dist.init_process_group("nccl", rank=self.proc_rank, world_size=self.world_size) + + def resolve_root_node_address(self, root_node): + if '[' in root_node: + name = root_node.split('[')[0] + number = root_node.split(',')[0] + if '-' in number: + number = number.split('-')[0] + + number = re.sub('[^0-9]', '', number) + root_node = name + number + + return root_node + + def __run_pretrain_routine(self, model): + """ + Sanity check a few things before starting actual training + :param model: + :return: + """ + ref_model = model + if self.data_parallel: + ref_model = model.module + + ref_model.trainer = self + + # set local properties on the model + ref_model.on_gpu = self.on_gpu + + # transfer data loaders from model + self.get_dataloaders(ref_model) + + # init training constants + self.__layout_bookeeping() + + # add lr schedulers + if self.lr_scheduler_milestones is not None: + for optimizer in self.optimizers: + scheduler = MultiStepLR(optimizer, self.lr_scheduler_milestones) + self.lr_schedulers.append(scheduler) + + # print model summary + if self.proc_rank == 0 and self.print_weights_summary: + ref_model.summarize() + + # give model convenience properties + ref_model.trainer = self + ref_model.experiment = self.experiment + + # run tiny validation to make sure program won't crash during val + _ = self.validate(model, self.val_dataloader, max_batches=self.nb_sanity_val_steps) + + # save exp to get started + if self.proc_rank == 0: + self.experiment.save() + + # track model now. + # if cluster resets state, the model will update with the saved weights + self.model = model + + # enable cluster checkpointing + # also restores training state + if self.cluster is not None: # pragma: no cover + self.enable_auto_hpc_walltime_manager() + + # --------------------------- + # CORE TRAINING LOOP + # --------------------------- + self.__train() + + def __train(self): + # run all epochs + for epoch_nb in range(self.current_epoch, self.max_nb_epochs): + # update the lr scheduler + for lr_scheduler in self.lr_schedulers: + lr_scheduler.step() + + model = self.__get_model() + model.current_epoch = epoch_nb + + # hook + if self.__is_function_implemented('on_epoch_start'): + model = self.__get_model() + model.on_epoch_start() + + self.current_epoch = epoch_nb + self.total_batches = self.nb_tng_batches + self.nb_val_batches + self.batch_loss_value = 0 # accumulated grads + + # init progbar when requested + if self.progress_bar: + self.prog_bar = tqdm.tqdm(range(self.total_batches), position=self.process_position) + + for batch_nb, data_batch in enumerate(self.tng_dataloader): + self.batch_nb = batch_nb + self.global_step += 1 + + model = self.__get_model() + model.global_step = self.global_step + + # stop when the flag is changed or we've gone past the amount requested in the batches + self.total_batch_nb += 1 + met_batch_limit = batch_nb > self.nb_tng_batches + if met_batch_limit: + break + + # --------------- + # RUN TRAIN STEP + # --------------- + batch_result = self.__run_tng_batch(data_batch, batch_nb) + early_stop_epoch = batch_result == -1 + + # --------------- + # RUN VAL STEP + # --------------- + is_val_check_batch = (batch_nb + 1) % self.val_check_batch == 0 + if self.fast_dev_run or is_val_check_batch or early_stop_epoch: + self.__run_validation() + + # when batch should be saved + if (batch_nb + 1) % self.log_save_interval == 0 or early_stop_epoch: + if self.proc_rank == 0: + self.experiment.save() + + # when metrics should be logged + if batch_nb % self.add_log_row_interval == 0 or early_stop_epoch: + # count items in memory + # nb_params, nb_tensors = count_mem_items() + + model = self.__get_model() + metrics = self.__tng_tqdm_dic + + # add gpu memory + if self.on_gpu: + mem_map = get_gpu_memory_map() + metrics.update(mem_map) + + # add norms + if self.track_grad_norm > 0: + model = self.__get_model() + grad_norm_dic = model.grad_norm(self.track_grad_norm) + metrics.update(grad_norm_dic) + + if self.__is_function_implemented('on_tng_metrics'): + model.on_tng_metrics(metrics) + + # log metrics + scalar_metrics = self.__metrics_to_scalars(metrics, blacklist=self.__log_vals_blacklist()) + if self.proc_rank == 0: + self.experiment.log(scalar_metrics, global_step=self.global_step) + self.experiment.save() + + # hook + if self.__is_function_implemented('on_batch_end'): + model = self.__get_model() + model.on_batch_end() + + # end epoch early + if early_stop_epoch: + break + + # hook + if self.__is_function_implemented('on_epoch_end'): + model = self.__get_model() + model.on_epoch_end() + + # early stopping + met_min_epochs = epoch_nb > self.min_nb_epochs + if self.enable_early_stop and met_min_epochs: + should_stop = self.early_stop_callback.on_epoch_end(epoch=epoch_nb, logs=self.__tng_tqdm_dic) + + # stop training + stop = should_stop and met_min_epochs + if stop: + return + + def __metrics_to_scalars(self, metrics, blacklist=[]): + new_metrics = {} + for k, v in metrics.items(): + if type(v) is torch.Tensor: + v = v.item() + + if type(v) is dict: + v = self.__metrics_to_scalars(v) + + if k not in blacklist: + new_metrics[k] = float(v) + + return new_metrics + + def __log_vals_blacklist(self): + """avoid logging some vals lightning uses to maintain state""" + blacklist = {'batch_nb', 'v_nb', 'gpu'} + return blacklist + + def __run_tng_batch(self, data_batch, batch_nb): + if data_batch is None: + return 0 + + # hook + if self.__is_function_implemented('on_batch_start'): + model_ref = self.__get_model() + response = model_ref.on_batch_start(data_batch) + + if response == -1: + return -1 + + if self.progress_bar: + self.prog_bar.update(1) + + # forward pass + # return a scalar value and a dic with tqdm metrics + if self.use_ddp: + output = self.model(data_batch, batch_nb) + elif self.use_dp: + output = self.model(data_batch, batch_nb) + output = reduce_distributed_output(output, len(self.data_parallel_device_ids)) + else: + output = self.model.training_step(data_batch, batch_nb) + + try: + model_specific_tqdm_metrics_dic = output['tqdm_metrics'] + except Exception as e: + model_specific_tqdm_metrics_dic = {} + + # if output dict doesn't have the keyword loss + # then assume the output=loss if scalar + try: + loss = output['loss'] + except Exception as e: + if type(output) is torch.Tensor: + loss = output + + self.__add_tqdm_metrics(model_specific_tqdm_metrics_dic) + + # backward pass + if self.use_amp: + # scale loss when using amp + for optimizer in self.optimizers: + with amp.scale_loss(loss, optimizer) as scaled_loss: + scaled_loss.backward() + else: + loss.backward() + + # insert after step hook + if self.__is_function_implemented('on_after_backward'): + model_ref = self.__get_model() + response = model_ref.on_after_backward() + + if self.print_nan_grads: + model = self.__get_model() + for param in model.parameters(): + print(param.grad.float().sum()) + + # avoid memory leaks + self.batch_loss_value += loss.item() + + # gradient update with accumulated gradients + if (self.batch_nb + 1) % self.accumulate_grad_batches == 0: + + # clip gradients + if self.gradient_clip > 0: + model = self.__get_model() + torch.nn.utils.clip_grad_norm(model.parameters(), self.gradient_clip) + + # update gradients across all optimizers + for optimizer in self.optimizers: + optimizer.step() + + # insert after step hook + if self.__is_function_implemented('on_before_zero_grad'): + model_ref = self.__get_model() + response = model_ref.on_before_zero_grad(optimizer) + + # clear gradients + optimizer.zero_grad() + + # queuing loss across batches blows it up proportionally... divide out the number accumulated + self.batch_loss_value = self.batch_loss_value / self.accumulate_grad_batches + + # track loss + self.running_loss.append(self.batch_loss_value) + self.batch_loss_value = 0 + self.avg_loss = np.mean(self.running_loss[-100:]) + + # update progbar + if self.progress_bar: + # add model specific metrics + tqdm_metrics = self.__tng_tqdm_dic + self.prog_bar.set_postfix(**tqdm_metrics) + + # activate batch end hook + if self.__is_function_implemented('on_batch_end'): + model = self.__get_model() + model.on_batch_end() + + return 0 + + def __run_validation(self): + # decide if can check epochs + can_check_epoch = (self.current_epoch + 1) % self.check_val_every_n_epoch == 0 + if self.fast_dev_run: + print('skipping to check performance bc of --fast_dev_run') + elif not can_check_epoch: + return + + # hook + if self.__is_function_implemented('on_pre_performance_check'): + model = self.__get_model() + model.on_pre_performance_check() + + # use full val set on end of epoch + # use a small portion otherwise + max_batches = None if not self.fast_dev_run else 1 + model_specific_tqdm_metrics_dic = self.validate( + self.model, + self.val_dataloader, + max_batches + ) + self.__add_tqdm_metrics(model_specific_tqdm_metrics_dic) + + # hook + if self.__is_function_implemented('on_post_performance_check'): + model = self.__get_model() + model.on_post_performance_check() + + if self.progress_bar: + # add model specific metrics + tqdm_metrics = self.__tng_tqdm_dic + self.prog_bar.set_postfix(**tqdm_metrics) + + # model checkpointing + if self.proc_rank == 0 and self.checkpoint_callback is not None: + print('save callback...') + self.checkpoint_callback.on_epoch_end(epoch=self.current_epoch, logs=self.__tng_tqdm_dic) \ No newline at end of file diff --git a/pytorch_lightning/pt_overrides/override_data_parallel.py b/pytorch_lightning/pt_overrides/override_data_parallel.py index ff4fce8c..89b550fd 100644 --- a/pytorch_lightning/pt_overrides/override_data_parallel.py +++ b/pytorch_lightning/pt_overrides/override_data_parallel.py @@ -1,6 +1,7 @@ from torch.nn import DataParallel from torch.nn.parallel import DistributedDataParallel import itertools +from itertools import chain import threading import torch @@ -8,7 +9,7 @@ from torch.cuda._utils import _get_device_index import pdb -def _find_tensors(obj): +def _find_tensors(obj): # pragma: no cover r""" Recursively find all tensors contained in the specified object. """ @@ -21,8 +22,7 @@ def _find_tensors(obj): return [] - -def get_a_var(obj): +def get_a_var(obj): # pragma: no cover if isinstance(obj, torch.Tensor): return obj @@ -42,6 +42,29 @@ class LightningDataParallel(DataParallel): Override the forward call in lightning so it goes to training and validation step respectively """ + def forward(self, *inputs, **kwargs): + if not self.device_ids: + return self.module(*inputs, **kwargs) + + for t in chain(self.module.parameters(), self.module.buffers()): + if t.device != self.src_device_obj: + raise RuntimeError("module must have its parameters and buffers " + "on device {} (device_ids[0]) but found one of " + "them on device: {}".format(self.src_device_obj, t.device)) + + inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids) + if len(self.device_ids) == 1: + # lightning + if self.module.training: + return self.module.training_step(*inputs[0], **kwargs[0]) + else: + return self.module.validation_step(*inputs[0], **kwargs[0]) + + replicas = self.replicate(self.module, self.device_ids[:len(inputs)]) + outputs = self.parallel_apply(replicas, inputs, kwargs) + return self.gather(outputs, self.output_device) + + def parallel_apply(self, replicas, inputs, kwargs): return parallel_apply(replicas, inputs, kwargs, self.device_ids[:len(replicas)]) @@ -54,7 +77,7 @@ class LightningDistributedDataParallel(DistributedDataParallel): def parallel_apply(self, replicas, inputs, kwargs): return parallel_apply(replicas, inputs, kwargs, self.device_ids[:len(replicas)]) - def forward(self, *inputs, **kwargs): + def forward(self, *inputs, **kwargs): # pragma: no cover self._sync_params() if self.device_ids: inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids) @@ -89,7 +112,7 @@ class LightningDistributedDataParallel(DistributedDataParallel): return output -def parallel_apply(modules, inputs, kwargs_tup=None, devices=None): +def parallel_apply(modules, inputs, kwargs_tup=None, devices=None): # pragma: no cover r"""Applies each `module` in :attr:`modules` in parallel on arguments contained in :attr:`inputs` (positional) and :attr:`kwargs_tup` (keyword) on each of :attr:`devices`. diff --git a/pytorch_lightning/root_module/decorators.py b/pytorch_lightning/root_module/decorators.py new file mode 100644 index 00000000..ef7bd502 --- /dev/null +++ b/pytorch_lightning/root_module/decorators.py @@ -0,0 +1,17 @@ + +def data_loader(fn): + """ + Decorator to make any fx with this use the lazy property + :param fn: + :return: + """ + + attr_name = '_lazy_' + fn.__name__ + + @property + def _data_loader(self): + if not hasattr(self, attr_name): + setattr(self, attr_name, fn(self)) + return getattr(self, attr_name) + + return _data_loader diff --git a/pytorch_lightning/root_module/grads.py b/pytorch_lightning/root_module/grads.py index e4d1701b..8ed17a3e 100644 --- a/pytorch_lightning/root_module/grads.py +++ b/pytorch_lightning/root_module/grads.py @@ -27,14 +27,3 @@ class GradInformation(nn.Module): results['grad_{}_norm_total'.format(norm_type)] = round(total_norm.data.cpu().numpy().flatten()[0], 3) return results - - def describe_grads(self): - for p in self.parameters(): - g = p.grad.data.numpy().flatten() - print(np.max(g), np.min(g), np.mean(g)) - - - def describe_params(self): - for p in self.parameters(): - g = p.data.numpy().flatten() - print(np.max(g), np.min(g), np.mean(g)) \ No newline at end of file diff --git a/pytorch_lightning/root_module/hooks.py b/pytorch_lightning/root_module/hooks.py index 99155ab9..88abe80d 100644 --- a/pytorch_lightning/root_module/hooks.py +++ b/pytorch_lightning/root_module/hooks.py @@ -19,3 +19,27 @@ class ModelHooks(torch.nn.Module): def on_post_performance_check(self): pass + def on_tng_metrics(self, metrics): + pass + + def on_before_zero_grad(self, optimizer): + """ + Called after optimizer.step() and before optimizer.zero_grad() + + for optimizer in optimizers: + optimizer.step() + model.on_before_zero_grad(optimizer) # < ---- called here + optimizer.zero_grad + + :param optimizer: + :return: + """ + pass + + def on_after_backward(self): + """ + Called after loss.backward() and before optimizers do anything + :return: + """ + pass + diff --git a/pytorch_lightning/root_module/memory.py b/pytorch_lightning/root_module/memory.py index 17f20efe..389c8680 100644 --- a/pytorch_lightning/root_module/memory.py +++ b/pytorch_lightning/root_module/memory.py @@ -33,33 +33,42 @@ class ModelSummary(object): mods = list(self.model.modules()) in_sizes = [] out_sizes = [] - input_ = self.example_input_array - for i in range(1, len(mods)): - m = mods[i] - if type(input_) is list or type(input_) is tuple: - out = m(*input_) - else: - out = m(input_) + input_ = self.model.example_input_array - if type(input_) is tuple or type(input_) is list: - in_size = [] - for x in input_: - if type(x) is list: - in_size.append(len(x)) - else: - in_size.append(x.size()) - else: - in_size = np.array(input_.size()) + if self.model.on_gpu: + input_ = input_.cuda(0) - in_sizes.append(in_size) + if self.model.trainer.use_amp: + input_ = input_.half() - if type(out) is tuple or type(out) is list: - out_size = np.asarray([x.size() for x in out]) - else: - out_size = np.array(out.size()) + with torch.no_grad(): - out_sizes.append(out_size) - input_ = out + for i in range(1, len(mods)): + m = mods[i] + if type(input_) is list or type(input_) is tuple: # pragma: no cover + out = m(*input_) + else: + out = m(input_) + + if type(input_) is tuple or type(input_) is list: # pragma: no cover + in_size = [] + for x in input_: + if type(x) is list: + in_size.append(len(x)) + else: + in_size.append(x.size()) + else: + in_size = np.array(input_.size()) + + in_sizes.append(in_size) + + if type(out) is tuple or type(out) is list: # pragma: no cover + out_size = np.asarray([x.size() for x in out]) + else: + out_size = np.array(out.size()) + + out_sizes.append(out_size) + input_ = out self.in_sizes = in_sizes self.out_sizes = out_sizes @@ -114,13 +123,22 @@ class ModelSummary(object): Layer Name, Layer Type, Input Size, Output Size, Number of Parameters ''' - df = pd.DataFrame( np.zeros( (len(self.layer_names), 3) ) ) - df.columns = ['Name', 'Type', 'Params'] + cols = ['Name', 'Type', 'Params'] + if self.model.example_input_array is not None: + cols.extend(['In_sizes', 'Out_sizes']) + + df = pd.DataFrame(np.zeros( (len(self.layer_names), len(cols)))) + df.columns = cols df['Name'] = self.layer_names df['Type'] = self.layer_types df['Params'] = self.param_nums + if self.model.example_input_array is not None: + + df['In_sizes'] = self.in_sizes + df['Out_sizes'] = self.out_sizes + self.summary = df return @@ -128,10 +146,13 @@ class ModelSummary(object): self.get_layer_names() self.get_parameter_sizes() self.get_parameter_nums() + + if self.model.example_input_array is not None: + self.get_variable_sizes() self.make_summary() -def print_mem_stack(): +def print_mem_stack(): # pragma: no cover for obj in gc.get_objects(): try: if torch.is_tensor(obj) or (hasattr(obj, 'data') and torch.is_tensor(obj.data)): @@ -140,7 +161,7 @@ def print_mem_stack(): pass -def count_mem_items(): +def count_mem_items(): # pragma: no cover nb_params = 0 nb_tensors = 0 for obj in gc.get_objects(): diff --git a/pytorch_lightning/root_module/model_saving.py b/pytorch_lightning/root_module/model_saving.py index 0fca9161..c5831317 100644 --- a/pytorch_lightning/root_module/model_saving.py +++ b/pytorch_lightning/root_module/model_saving.py @@ -2,36 +2,38 @@ import torch import os import re import pdb -from pytorch_lightning.pt_overrides.override_data_parallel import LightningDistributedDataParallel +from pytorch_lightning.pt_overrides.override_data_parallel import LightningDistributedDataParallel, LightningDataParallel + class ModelIO(object): - def load_model_specific(self, checkpoint): + def on_load_checkpoint(self, checkpoint): """ Do something with the checkpoint + Gives model a chance to load something before state_dict is restored :param checkpoint: :return: """ - raise NotImplementedError + pass - def get_save_dict(self): + def on_save_checkpoint(self, checkpoint): """ - Return specific things for the model - :return: + Give the model a chance to add something to the checkpoint. + state_dict is already there """ - raise NotImplementedError + pass # ------------------------- # OPTIONAL HOOKS # ------------------------- - def on_hpc_save(self): + def on_hpc_save(self, checkpoint): """ Hook to do whatever you need right before Slurm manager saves the model :return: """ pass - def on_hpc_load(self): + def on_hpc_load(self, checkpoint): """ Hook to do whatever you need right before Slurm manager loads the model :return: @@ -41,6 +43,11 @@ class ModelIO(object): class TrainerIO(object): + def __get_model(self): + is_dp_module = type(self.model) is LightningDistributedDataParallel or type(self.model) is LightningDataParallel + model = self.model.module if is_dp_module else self.model + return model + # -------------------- # MODEL SAVE CHECKPOINT # -------------------- @@ -51,26 +58,32 @@ class TrainerIO(object): torch.save(checkpoint, filepath) def dump_checkpoint(self): + checkpoint = { 'epoch': self.current_epoch, - 'checkpoint_callback_best': self.checkpoint_callback.best, - 'early_stop_callback_wait': self.early_stop_callback.wait, - 'early_stop_callback_patience': self.early_stop_callback.patience, 'global_step': self.global_step } + if self.checkpoint_callback is not None: + checkpoint['checkpoint_callback_best'] = self.checkpoint_callback.best + + if self.early_stop_callback is not None: + checkpoint['early_stop_callback_wait'] = self.early_stop_callback.wait + checkpoint['early_stop_callback_patience'] = self.early_stop_callback.patience + optimizer_states = [] for i, optimizer in enumerate(self.optimizers): optimizer_states.append(optimizer.state_dict()) checkpoint['optimizer_states'] = optimizer_states - # request what to save from the model - model = self.model.module if type(self.model) is LightningDistributedDataParallel else self.model - checkpoint_dict = model.get_save_dict() + # add the state_dict from the model + model = self.__get_model() + checkpoint['state_dict'] = model.state_dict() + + # give the model a chance to add a few things + model.on_save_checkpoint(checkpoint) - # merge trainer and model saving items - checkpoint.update(checkpoint_dict) return checkpoint # -------------------- @@ -103,9 +116,13 @@ class TrainerIO(object): :param checkpoint: :return: """ - self.checkpoint_callback.best = checkpoint['checkpoint_callback_best'] - self.early_stop_callback.wait = checkpoint['early_stop_callback_wait'] - self.early_stop_callback.patience = checkpoint['early_stop_callback_patience'] + if self.checkpoint_callback is not None: + self.checkpoint_callback.best = checkpoint['checkpoint_callback_best'] + + if self.early_stop_callback is not None: + self.early_stop_callback.wait = checkpoint['early_stop_callback_wait'] + self.early_stop_callback.patience = checkpoint['early_stop_callback_patience'] + self.global_step = checkpoint['global_step'] self.current_epoch = checkpoint['epoch'] @@ -134,13 +151,15 @@ class TrainerIO(object): filepath = '{}/hpc_ckpt_{}.ckpt'.format(folderpath, ckpt_number) # give model a chance to do something on hpc_save - self.on_hpc_save() + model = self.__get_model() + checkpoint = self.dump_checkpoint() - # request what to save from the model - checkpoint_dict = self.dump_checkpoint() + model.on_hpc_save(checkpoint) # do the actual save - torch.save(checkpoint_dict, filepath) + torch.save(checkpoint, filepath) + + return filepath def hpc_load(self, folderpath, on_gpu): filepath = '{}/hpc_ckpt_{}.ckpt'.format(folderpath, self.max_ckpt_in_folder(folderpath)) @@ -150,15 +169,17 @@ class TrainerIO(object): else: checkpoint = torch.load(filepath, map_location=lambda storage, loc: storage) - # load training state + # load training state (affects trainer only) self.restore_training_state(checkpoint) # load model state - model = self.model.module if type(self.model) is LightningDataParallel else self.model - model.load_model_specific(checkpoint) + model = self.__get_model() + + # load the state_dict on the model automatically + model.load_state_dict(checkpoint['state_dict']) # call model hook - self.on_hpc_load() + model.on_hpc_load(checkpoint) def max_ckpt_in_folder(self, path): files = os.listdir(path) diff --git a/pytorch_lightning/root_module/optimization.py b/pytorch_lightning/root_module/optimization.py deleted file mode 100644 index 3172e1a1..00000000 --- a/pytorch_lightning/root_module/optimization.py +++ /dev/null @@ -1,22 +0,0 @@ -from torch import nn -from torch import optim - - -class OptimizerConfig(nn.Module): - - def choose_optimizer(self, optimizer, params, optimizer_params, opt_name_key): - if optimizer == 'adam': - optimizer = optim.Adam(params, **optimizer_params) - if optimizer == 'sparse_adam': - optimizer = optim.SparseAdam(params, **optimizer_params) - if optimizer == 'sgd': - optimizer = optim.SGD(params, **optimizer_params) - if optimizer == 'adadelta': - optimizer = optim.Adadelta(params, **optimizer_params) - - # transfer opt state if loaded - if opt_name_key in self.loaded_optimizer_states_dict: - state = self.loaded_optimizer_states_dict[opt_name_key] - optimizer.load_state_dict(state) - - return optimizer diff --git a/pytorch_lightning/root_module/root_module.py b/pytorch_lightning/root_module/root_module.py index ef43e09a..b49dd3af 100644 --- a/pytorch_lightning/root_module/root_module.py +++ b/pytorch_lightning/root_module/root_module.py @@ -1,19 +1,15 @@ -import os import torch -import math - from pytorch_lightning.root_module.memory import ModelSummary from pytorch_lightning.root_module.grads import GradInformation from pytorch_lightning.root_module.model_saving import ModelIO, load_hparams_from_tags_csv -from pytorch_lightning.root_module.optimization import OptimizerConfig from pytorch_lightning.root_module.hooks import ModelHooks +from pytorch_lightning.root_module.decorators import data_loader -class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): +class LightningModule(GradInformation, ModelIO, ModelHooks): - def __init__(self, hparams): - super(LightningModule, self).__init__() - self.hparams = hparams + def __init__(self, *args, **kwargs): + super(LightningModule, self).__init__(*args, **kwargs) self.dtype = torch.FloatTensor self.exp_save_path = None @@ -22,15 +18,11 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): self.loaded_optimizer_states_dict = {} self.trainer = None self.experiment = None + self.example_input_array = None # track if gpu was requested for checkpointing self.on_gpu = False - # computed vars for the dataloaders - self._tng_dataloader = None - self._val_dataloader = None - self._test_dataloader = None - def forward(self, *args, **kwargs): """ Expand model in into whatever you need. @@ -71,37 +63,7 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): """ raise NotImplementedError - def update_tng_log_metrics(self, logs): - """ - Chance to update metrics to be logged for training step. - For example, add music, images, etc... to log - :param logs: - :return: - """ - raise NotImplementedError - - def loss(self, *args, **kwargs): - """ - Expand model_out into your components - :param model_out: - :return: - """ - raise NotImplementedError - - def summarize(self): - model_summary = ModelSummary(self) - print(model_summary) - - - def freeze(self): - for param in self.parameters(): - param.requires_grad = False - - def unfreeze(self): - for param in self.parameters(): - param.requires_grad = True - - @property + @data_loader def tng_dataloader(self): """ Implement a function to load an h5py of this data @@ -109,7 +71,7 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): """ raise NotImplementedError - @property + @data_loader def test_dataloader(self): """ Implement a function to load an h5py of this data @@ -117,7 +79,7 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): """ raise NotImplementedError - @property + @data_loader def val_dataloader(self): """ Implement a function to load an h5py of this data @@ -125,16 +87,6 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): """ raise NotImplementedError - @staticmethod - def get_process_position(gpus): - try: - current_gpu = os.environ["CUDA_VISIBLE_DEVICES"] - gpu_ids = gpus.split(';') - process_position = gpu_ids.index(current_gpu) - return process_position, current_gpu - except Exception as e: - return 0, 0 - @classmethod def load_from_metrics(cls, weights_path, tags_csv, on_gpu, map_location=None): """ @@ -156,9 +108,26 @@ class LightningModule(GradInformation, ModelIO, OptimizerConfig, ModelHooks): else: checkpoint = torch.load(weights_path, map_location=lambda storage, loc: storage) + # load the state_dict on the model automatically model = cls(hparams) + model.load_state_dict(checkpoint['state_dict']) + + # give model a chance to load something + model.on_load_checkpoint(checkpoint) - # allow model to load - model.load_model_specific(checkpoint) - model.load_state_dict(checkpoint['state_dict'], strict=False) return model + + def summarize(self): + model_summary = ModelSummary(self) + print(model_summary) + + def freeze(self): + for param in self.parameters(): + param.requires_grad = False + + def unfreeze(self): + for param in self.parameters(): + param.requires_grad = True + + + diff --git a/pytorch_lightning/testing_models/__init__.py b/pytorch_lightning/testing_models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pytorch_lightning/testing_models/lm_test_module.py b/pytorch_lightning/testing_models/lm_test_module.py new file mode 100644 index 00000000..39fcbb4c --- /dev/null +++ b/pytorch_lightning/testing_models/lm_test_module.py @@ -0,0 +1,253 @@ +import os +from collections import OrderedDict +import torch.nn as nn +from torchvision.datasets import MNIST +import torchvision.transforms as transforms +import torch +import torch.nn.functional as F +from test_tube import HyperOptArgumentParser +from torch import optim +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler + +from pytorch_lightning.root_module.root_module import LightningModule +import pytorch_lightning as ptl + + +class LightningTestModel(LightningModule): + """ + Sample model to show how to define a template + """ + + def __init__(self, hparams, force_remove_distributed_sampler=False): + """ + Pass in parsed HyperOptArgumentParser to the model + :param hparams: + """ + # init superclass + super(LightningTestModel, self).__init__() + self.hparams = hparams + + self.batch_size = hparams.batch_size + + # if you specify an example input, the summary will show input/output for each layer + self.example_input_array = torch.rand(5, 28 * 28) + + # remove to test warning for dist sampler + self.force_remove_distributed_sampler = force_remove_distributed_sampler + + # build model + self.__build_model() + + # --------------------- + # MODEL SETUP + # --------------------- + def __build_model(self): + """ + Layout model + :return: + """ + self.c_d1 = nn.Linear(in_features=self.hparams.in_features, out_features=self.hparams.hidden_dim) + self.c_d1_bn = nn.BatchNorm1d(self.hparams.hidden_dim) + self.c_d1_drop = nn.Dropout(self.hparams.drop_prob) + + self.c_d2 = nn.Linear(in_features=self.hparams.hidden_dim, out_features=self.hparams.out_features) + + # --------------------- + # TRAINING + # --------------------- + def forward(self, x): + """ + No special modification required for lightning, define as you normally would + :param x: + :return: + """ + + x = self.c_d1(x) + x = torch.tanh(x) + x = self.c_d1_bn(x) + x = self.c_d1_drop(x) + + x = self.c_d2(x) + logits = F.log_softmax(x, dim=1) + + return logits + + def loss(self, labels, logits): + nll = F.nll_loss(logits, labels) + return nll + + def training_step(self, data_batch, batch_i): + """ + Lightning calls this inside the training loop + :param data_batch: + :return: + """ + # forward pass + x, y = data_batch + x = x.view(x.size(0), -1) + + y_hat = self.forward(x) + + # calculate loss + loss_val = self.loss(y, y_hat) + + # in DP mode (default) make sure if result is scalar, there's another dim in the beginning + if self.trainer.use_dp: + loss_val = loss_val.unsqueeze(0) + + output = OrderedDict({ + 'loss': loss_val + }) + + # can also return just a scalar instead of a dict (return loss_val) + return output + + def validation_step(self, data_batch, batch_i): + """ + Lightning calls this inside the validation loop + :param data_batch: + :return: + """ + x, y = data_batch + x = x.view(x.size(0), -1) + y_hat = self.forward(x) + + loss_val = self.loss(y, y_hat) + + # acc + labels_hat = torch.argmax(y_hat, dim=1) + val_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + val_acc = torch.tensor(val_acc) + + if self.on_gpu: + val_acc = val_acc.cuda(loss_val.device.index) + + # in DP mode (default) make sure if result is scalar, there's another dim in the beginning + if self.trainer.use_dp: + loss_val = loss_val.unsqueeze(0) + val_acc = val_acc.unsqueeze(0) + + # alternate possible outputs to test + if self.trainer.batch_nb % 1 == 0: + output = OrderedDict({ + 'val_loss': loss_val, + 'val_acc': val_acc, + }) + return output + if self.trainer.batch_nb % 2 == 0: + return val_acc + + if self.trainer.batch_nb % 3 == 0: + output = OrderedDict({ + 'val_loss': loss_val, + 'val_acc': val_acc, + 'test_dic': {'val_loss_a': loss_val} + }) + return output + + def validation_end(self, outputs): + """ + Called at the end of validation to aggregate outputs + :param outputs: list of individual outputs of each validation step + :return: + """ + # if returned a scalar from validation_step, outputs is a list of tensor scalars + # we return just the average in this case (if we want) + # return torch.stack(outputs).mean() + + val_loss_mean = 0 + val_acc_mean = 0 + for output in outputs: + val_loss_mean += output['val_loss'] + val_acc_mean += output['val_acc'] + + val_loss_mean /= len(outputs) + val_acc_mean /= len(outputs) + + tqdm_dic = {'val_loss': val_loss_mean.item(), 'val_acc': val_acc_mean.item()} + return tqdm_dic + + def on_tng_metrics(self, logs): + logs['some_tensor_to_test'] = torch.rand(1) + + # --------------------- + # TRAINING SETUP + # --------------------- + def configure_optimizers(self): + """ + return whatever optimizers we want here + :return: list of optimizers + """ + optimizer = optim.Adam(self.parameters(), lr=self.hparams.learning_rate) + return [optimizer] + + def __dataloader(self, train): + # init data generators + transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (1.0,))]) + dataset = MNIST(root=self.hparams.data_root, train=train, transform=transform, download=True) + + # when using multi-node we need to add the datasampler + train_sampler = None + batch_size = self.hparams.batch_size + + try: + if self.on_gpu and not self.force_remove_distributed_sampler: + train_sampler = DistributedSampler(dataset, rank=self.trainer.proc_rank) + batch_size = batch_size // self.trainer.world_size # scale batch size + except Exception as e: + pass + + should_shuffle = train_sampler is None + loader = DataLoader( + dataset=dataset, + batch_size=batch_size, + shuffle=should_shuffle, + sampler=train_sampler + ) + + return loader + + @ptl.data_loader + def tng_dataloader(self): + return self.__dataloader(train=True) + + @ptl.data_loader + def val_dataloader(self): + return self.__dataloader(train=False) + + @ptl.data_loader + def test_dataloader(self): + return self.__dataloader(train=False) + + @staticmethod + def add_model_specific_args(parent_parser, root_dir): + """ + Parameters you define here will be available to your model through self.hparams + :param parent_parser: + :param root_dir: + :return: + """ + parser = HyperOptArgumentParser(strategy=parent_parser.strategy, parents=[parent_parser]) + + # param overwrites + # parser.set_defaults(gradient_clip=5.0) + + # network params + parser.opt_list('--drop_prob', default=0.2, options=[0.2, 0.5], type=float, tunable=False) + parser.add_argument('--in_features', default=28*28, type=int) + parser.add_argument('--out_features', default=10, type=int) + parser.add_argument('--hidden_dim', default=50000, type=int) # use 500 for CPU, 50000 for GPU to see speed difference + + # data + parser.add_argument('--data_root', default=os.path.join(root_dir, 'mnist'), type=str) + + # training params (opt) + parser.opt_list('--learning_rate', default=0.001*8, type=float, options=[0.0001, 0.0005, 0.001, 0.005], + tunable=False) + parser.opt_list('--optimizer_name', default='adam', type=str, options=['adam'], tunable=False) + + # if using 2 nodes with 4 gpus each the batch size here (256) will be 256 / (2*8) = 16 per gpu + parser.opt_list('--batch_size', default=256*8, type=int, options=[32, 64, 128, 256], tunable=False, + help='batch size will be divided over all the gpus being used across all nodes') + return parser diff --git a/pytorch_lightning/trainer_main.py b/pytorch_lightning/trainer_main.py index 8389b5ec..0c30a419 100644 --- a/pytorch_lightning/trainer_main.py +++ b/pytorch_lightning/trainer_main.py @@ -52,10 +52,6 @@ def main(hparams, cluster, results_dict): hparams.__setattr__('nb_gpus', torch.cuda.device_count()) hparams.__setattr__('inference_mode', hparams.model_load_weights_path is not None) - # delay each training start to not overwrite logs - process_position, current_gpu = TRAINING_MODEL.get_process_position(hparams.gpus) - sleep(process_position + 1) - # init experiment exp = Experiment( name=hparams.tt_name, diff --git a/pytorch_lightning/utils/debugging.py b/pytorch_lightning/utils/debugging.py new file mode 100644 index 00000000..3091ff3b --- /dev/null +++ b/pytorch_lightning/utils/debugging.py @@ -0,0 +1,5 @@ +import pdb +import sys + +class MisconfigurationException(Exception): + pass \ No newline at end of file diff --git a/pytorch_lightning/utils/embeddings.py b/pytorch_lightning/utils/embeddings.py deleted file mode 100644 index 4e96c61b..00000000 --- a/pytorch_lightning/utils/embeddings.py +++ /dev/null @@ -1,104 +0,0 @@ -import torch -import numpy as np -from copy import deepcopy - - -class PretrainedEmbedding(torch.nn.Embedding): - - def __init__(self, embedding_path, embedding_dim, task_vocab, freeze=True, *args, **kwargs): - """ - Loads a prebuilt pytorch embedding from any embedding formated file. - Padding=0 by default. - - >>> emb = PretrainedEmbedding(embedding_path='glove.840B.300d.txt',embedding_dim=300, task_vocab={'hello': 1, 'world': 2}) - >>> data = torch.Tensor([[0, 1], [0, 2]]).long() - >>> embedded = emb(data) - - - - :param embedding_path: - :param emb_dim: - :param task_vocab: - :param freeze: - :return: - """ - # count the vocab - self.vocab_size = max(task_vocab.values()) + 1 - super(PretrainedEmbedding, self).__init__(self.vocab_size, embedding_dim, padding_idx=0, *args, **kwargs) - - # load pretrained embeddings - new_emb = self.__load_task_specific_embeddings(deepcopy(task_vocab), embedding_path, embedding_dim, freeze) - - # transfer weights - self.weight = new_emb.weight - - # apply freeze - should_freeze = not freeze - self.weight.requires_grad = should_freeze - - def __load_task_specific_embeddings(self, vocab_words, embedding_path, emb_dim, freeze): - """ - Iterates embedding file to only pull out task specific embeddings - :param vocab_words: - :param embedding_path: - :param emb_dim: - :param freeze: - :return: - """ - - # holds final embeddings for relevant words - embeddings = np.zeros(shape=(self.vocab_size, emb_dim)) - - # load embedding line by line and extract relevant embeddings - with open(embedding_path, encoding='utf-8') as f: - for line in f: - tokens = line.split(' ') - word = tokens[0] - embedding = tokens[1:] - embedding[-1] = embedding[-1][:-1] # remove last new line - - if word in vocab_words: - vocab_word_i = vocab_words[word] - - # skip words that try to overwrite pad idx - if vocab_word_i == 0: - del vocab_words[word] - continue - - emb_vals = np.asarray([float(x) for x in embedding]) - embeddings[vocab_word_i] = emb_vals - - # remove vocab word to early terminate - del vocab_words[word] - - # early break - if len(vocab_words) == 0: - break - - # add random vectors for the non-pretrained words - # these are vocab words NOT found in the pretrained embeddings - for w, i in vocab_words.items(): - # skip words that try to overwrite pad idx - if i == 0: - continue - - embedding = np.random.normal(size=emb_dim) - embeddings[i] = embedding - - # turn into pt embedding - embeddings = torch.FloatTensor(embeddings) - embeddings = torch.nn.Embedding.from_pretrained(embeddings, freeze=freeze) - - return embeddings - - -if __name__ == '__main__': - emb = PretrainedEmbedding( - embedding_path='/Users/waf/Developer', - embedding_dim=300, - task_vocab={'hello': 1, 'world': 2} - ) - - data = torch.Tensor([[0, 1], [0, 2]]).long() - embedded = emb(data) - print(embedded) diff --git a/pytorch_lightning/utils/plotting.py b/pytorch_lightning/utils/plotting.py deleted file mode 100644 index 95fe64cc..00000000 --- a/pytorch_lightning/utils/plotting.py +++ /dev/null @@ -1,28 +0,0 @@ -from matplotlib import pyplot as plt -import numpy as np -np.seterr(divide='ignore', invalid='ignore') - - -def plot_confusion_matrix(cm, - save_path, - normalize=False, - title='Confusion matrix', - ylabel='y', - xlabel='x'): - """ - This function prints and plots the confusion matrix. - Normalization can be applied by setting `normalize=True`. - """ - if normalize: - cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis] - print("Normalized confusion matrix") - else: - print('Confusion matrix, without normalization') - - fig = plt.figure() - plt.matshow(cm) - plt.title(title) - plt.colorbar() - plt.ylabel(ylabel) - plt.xlabel(xlabel) - plt.savefig(save_path) diff --git a/requirements.txt b/requirements.txt index 730be99c..3e863f05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,9 @@ - -atomicwrites==1.2.1 -attrs==18.2.0 -certifi==2018.11.29 -cffi==1.11.5 -imageio==2.4.1 -mkl-fft==1.0.6 -mkl-random==1.0.2 -more-itertools==5.0.0 -numpy==1.15.4 -olefile==0.46 -pandas==0.23.4 -Pillow==5.3.0 -pluggy==0.8.0 -py==1.7.0 -pycparser==2.19 -pytest==4.0.2 -python-dateutil==2.7.5 -pytz==2018.7 +coverage==4.5.3 +mkdocs==1.0.4 +pytest==5.0.1 scikit-learn==0.20.2 -scipy==1.2.0 -six==1.12.0 -sklearn==0.0 -tensorboard==1.14.0 -tensorboardX==1.7 -tensorflow==1.14.0 -test-tube==0.643 -torch==1.0.0 -torchvision==0.2.1 tqdm==4.32.1 twine==1.13.0 -urllib3==1.25.3 -webencodings==0.5.1 -Werkzeug==0.15.4 -wrapt==1.11.2 +numpy==1.16.4 +torch>=1.1.0 +torchvision==0.3.0 diff --git a/setup.cfg b/setup.cfg index c7616dab..5b21c683 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,33 @@ markers = ignore = E731,W504 max-line-length = 120 +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + except Exception as e + print(e) + print(traceback.print_exc()) + return * + raise Exception + warnings + print + raise RuntimeError + break + pass + os.makedirs + +omit = + pytorch_lightning/callbacks/pt_callbacks.py + tests/test_models.py + pytorch_lightning/testing_models/lm_test_module.py + [flake8] ignore = E731,W504,F401,F841 max-line-length = 120 diff --git a/setup.py b/setup.py index 5fe9ac8b..50a05057 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages # http://blog.ionelmc.ro/2014/05/25/python-packaging/ setup( name="pytorch-lightning", - version='0.21', + version='0.3.6.4', description="The Keras for ML researchers using PyTorch", author="William Falcon", author_email="waf2107@columbia.edu", @@ -19,8 +19,7 @@ setup( install_requires=[ "torch>=1.1.0", "tqdm", - "test-tube>=0.653", - "tensorflow>=1.14.0" + "test-tube>=0.6.7.4", ], packages=find_packages(), long_description=open("README.md", encoding="utf-8").read(), diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..20f783bf --- /dev/null +++ b/tests/README.md @@ -0,0 +1,58 @@ +# Pytorch-Lightning Tests + +## Running tests +The automatic travis tests ONLY run CPU-based tests. Although these cover most of the use cases, +run on a 2-GPU machine to validate the full test-suite. + + +To run all tests do the following: +```bash +git clone https://github.com/williamFalcon/pytorch-lightning +cd pytorch-lightning + +# install module locally +pip install -e . + +# install dev deps +pip install -r requirements.txt + +# run tests +py.test +``` + +To test models that require GPU make sure to run the above command on a GPU machine. +The GPU machine must have: +1. At least 2 GPUs. +2. [NVIDIA-apex](https://github.com/NVIDIA/apex#linux) installed. + + +### test_models.py +This file fits a tiny model on MNIST using these different set-ups. +1. CPU only. +2. Single GPU with DP. +3. Multiple (2) GPUs using DP. +3. Multiple (2) GPUs using DDP. +3. Multiple (2) GPUs using DP + apex (for 16-bit precision). +3. Multiple (2) GPUs using DDP + apex (for 16-bit precision). + +For each set up it also tests: +1. model saving. +2. model loading. +3. predicting with a loaded model. +4. simulated save from HPC signal. +5. simulated load from HPC signal. + +## Running Coverage + +```bash +cd pytorch-lightning + +# generate coverage +pip install coverage +coverage run tests/test_models.py + +# print coverage stats +coverage report -m +``` + + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/debug.py b/tests/debug.py new file mode 100644 index 00000000..c4c3ffd1 --- /dev/null +++ b/tests/debug.py @@ -0,0 +1,180 @@ +import pytest +from pytorch_lightning import Trainer +from pytorch_lightning.examples.new_project_templates.lightning_module_template import LightningTemplateModel +from argparse import Namespace +from test_tube import Experiment +from pytorch_lightning.callbacks import ModelCheckpoint +import numpy as np +import warnings +import torch +import os +import shutil +import pdb + +import pytorch_lightning as ptl +import torch +from torch.nn import functional as F +from torch.utils.data import DataLoader +from torchvision.datasets import MNIST + + +class CoolModel(ptl.LightningModule): + + def __init(self): + super(CoolModel, self).__init__() + # not the best model... + self.l1 = torch.nn.Linear(28 * 28, 10) + + def forward(self, x): + return torch.relu(self.l1(x)) + + def my_loss(self, y_hat, y): + return F.cross_entropy(y_hat, y) + + def training_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'tng_loss': self.my_loss(y_hat, y)} + + def validation_step(self, batch, batch_nb): + x, y = batch + y_hat = self.forward(x) + return {'val_loss': self.my_loss(y_hat, y)} + + def validation_end(self, outputs): + avg_loss = torch.stack([x for x in outputs['val_loss']]).mean() + return avg_loss + + def configure_optimizers(self): + return [torch.optim.Adam(self.parameters(), lr=0.02)] + + @ptl.data_loader + def tng_dataloader(self): + return DataLoader(MNIST('path/to/save', train=True), batch_size=32) + + @ptl.data_loader + def val_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) + + @ptl.data_loader + def test_dataloader(self): + return DataLoader(MNIST('path/to/save', train=False), batch_size=32) + + +def get_model(): + # set up model with these hyperparams + root_dir = os.path.dirname(os.path.realpath(__file__)) + hparams = Namespace(**{'drop_prob': 0.2, + 'batch_size': 32, + 'in_features': 28*28, + 'learning_rate': 0.001*8, + 'optimizer_name': 'adam', + 'data_root': os.path.join(root_dir, 'mnist'), + 'out_features': 10, + 'hidden_dim': 1000}) + model = LightningTemplateModel(hparams) + + return model, hparams + + +def get_exp(debug=True): + # set up exp object without actually saving logs + root_dir = os.path.dirname(os.path.realpath(__file__)) + exp = Experiment(debug=debug, save_dir=root_dir, name='tests_tt_dir') + return exp + + +def init_save_dir(): + root_dir = os.path.dirname(os.path.realpath(__file__)) + save_dir = os.path.join(root_dir, 'save_dir') + + if os.path.exists(save_dir): + shutil.rmtree(save_dir) + + os.makedirs(save_dir, exist_ok=True) + + return save_dir + + +def clear_save_dir(): + root_dir = os.path.dirname(os.path.realpath(__file__)) + save_dir = os.path.join(root_dir, 'save_dir') + if os.path.exists(save_dir): + shutil.rmtree(save_dir) + + +def load_model(exp, save_dir): + + # load trained model + tags_path = exp.get_data_path(exp.name, exp.version) + tags_path = os.path.join(tags_path, 'meta_tags.csv') + + checkpoints = [x for x in os.listdir(save_dir) if '.ckpt' in x] + weights_dir = os.path.join(save_dir, checkpoints[0]) + + trained_model = LightningTemplateModel.load_from_metrics(weights_path=weights_dir, tags_csv=tags_path, on_gpu=True) + + assert trained_model is not None, 'loading model failed' + + return trained_model + + +def run_prediction(dataloader, trained_model): + # run prediction on 1 batch + for batch in dataloader: + break + + x, y = batch + x = x.view(x.size(0), -1) + + y_hat = trained_model(x) + + # acc + labels_hat = torch.argmax(y_hat, dim=1) + val_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + val_acc = torch.tensor(val_acc) + val_acc = val_acc.item() + + print(val_acc) + + assert val_acc > 0.70, f'this model is expected to get > 0.7 in test set (it got {val_acc})' + + +def main(): + + save_dir = init_save_dir() + + # exp file to get meta + exp = get_exp(False) + exp.save() + + # exp file to get weights + checkpoint = ModelCheckpoint(save_dir) + + trainer = Trainer( + experiment=exp, + checkpoint_callback=checkpoint, + progress_bar=True, + max_nb_epochs=1, + gpus=[0, 1], + distributed_backend='dp', + ) + + model = CoolModel() + + result = trainer.fit(model) + + # correct result and ok accuracy + assert result == 1, 'amp + ddp model failed to complete' + + # test model loading + pretrained_model = load_model(exp, save_dir) + + # test model preds + run_prediction(model.test_dataloader, pretrained_model) + + clear_save_dir() + + +if __name__ == '__main__': + main() diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..e8fa339b --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,688 @@ +import pytest +from pytorch_lightning import Trainer +from pytorch_lightning.examples.new_project_templates.lightning_module_template import LightningTemplateModel +from pytorch_lightning.testing_models.lm_test_module import LightningTestModel +from argparse import Namespace +from test_tube import Experiment, SlurmCluster +from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping +from pytorch_lightning.utils.debugging import MisconfigurationException +from pytorch_lightning.root_module import memory +from pytorch_lightning.models.trainer import reduce_distributed_output +from pytorch_lightning.root_module import model_saving +import numpy as np +import warnings +import torch +import os +import shutil +import pdb + +SEED = 2334 +torch.manual_seed(SEED) +np.random.seed(SEED) + + +# ------------------------------------------------------------------------ +# TESTS +# ------------------------------------------------------------------------ + +def test_cpu_slurm_save_load(): + """ + Verify model save/load/checkpoint on CPU + :return: + """ + hparams = get_hparams() + model = LightningTestModel(hparams) + + save_dir = init_save_dir() + + # exp file to get meta + exp = get_exp(False) + exp.argparse(hparams) + exp.save() + + cluster_a = SlurmCluster() + trainer_options = dict( + max_nb_epochs=1, + cluster=cluster_a, + experiment=exp, + checkpoint_callback=ModelCheckpoint(save_dir) + ) + + # fit model + trainer = Trainer(**trainer_options) + result = trainer.fit(model) + real_global_step = trainer.global_step + + # traning complete + assert result == 1, 'amp + ddp model failed to complete' + + # predict with trained model before saving + # make a prediction + for batch in model.test_dataloader: + break + + x, y = batch + x = x.view(x.size(0), -1) + + model.eval() + pred_before_saving = model(x) + + # test registering a save function + trainer.enable_auto_hpc_walltime_manager() + + # test HPC saving + # simulate snapshot on slurm + saved_filepath = trainer.hpc_save(save_dir, exp) + assert os.path.exists(saved_filepath) + + # wipe-out trainer and model + # retrain with not much data... this simulates picking training back up after slurm + # we want to see if the weights come back correctly + continue_tng_hparams = get_hparams(continue_training=True, hpc_exp_number=cluster_a.hpc_exp_number) + trainer_options = dict( + max_nb_epochs=1, + cluster=SlurmCluster(continue_tng_hparams), + experiment=exp, + checkpoint_callback=ModelCheckpoint(save_dir), + ) + trainer = Trainer(**trainer_options) + model = LightningTestModel(hparams) + + # set the epoch start hook so we can predict before the model does the full training + def assert_pred_same(): + assert trainer.global_step == real_global_step and trainer.global_step > 0 + + # predict with loaded model to make sure answers are the same + trainer.model.eval() + new_pred = trainer.model(x) + assert torch.all(torch.eq(pred_before_saving, new_pred)).item() == 1 + + model.on_epoch_start = assert_pred_same + + # by calling fit again, we trigger training, loading weights from the cluster + # and our hook to predict using current model before any more weight updates + trainer.fit(model) + + clear_save_dir() + + +def test_loading_meta_tags(): + hparams = get_hparams() + + save_dir = init_save_dir() + + # save tags + exp = get_exp(False) + exp.tag({'some_str':'a_str', 'an_int': 1, 'a_float': 2.0}) + exp.argparse(hparams) + exp.save() + + # load tags + tags_path = exp.get_data_path(exp.name, exp.version) + '/meta_tags.csv' + tags = model_saving.load_hparams_from_tags_csv(tags_path) + + assert tags.batch_size == 32 and tags.hidden_dim == 1000 + + clear_save_dir() + + +def test_dp_output_reduce(): + + # test identity when we have a single gpu + out = torch.rand(3, 1) + assert reduce_distributed_output(out, nb_gpus=1) is out + + # average when we have multiples + assert reduce_distributed_output(out, nb_gpus=2) == out.mean() + + # when we have a dict of vals + out = { + 'a': out, + 'b': { + 'c': out + } + } + reduced = reduce_distributed_output(out, nb_gpus=3) + assert reduced['a'] == out['a'] + assert reduced['b']['c'] == out['b']['c'] + + +def test_model_saving_loading(): + """ + Tests use case where trainer saves the model, and user loads it from tags independently + :return: + """ + hparams = get_hparams() + model = LightningTestModel(hparams) + + save_dir = init_save_dir() + + # exp file to get meta + exp = get_exp(False) + exp.argparse(hparams) + exp.save() + + trainer_options = dict( + max_nb_epochs=1, + cluster=SlurmCluster(), + experiment=exp, + checkpoint_callback=ModelCheckpoint(save_dir) + ) + + # fit model + trainer = Trainer(**trainer_options) + result = trainer.fit(model) + + # traning complete + assert result == 1, 'amp + ddp model failed to complete' + + # make a prediction + for batch in model.test_dataloader: + break + + x, y = batch + x = x.view(x.size(0), -1) + + # generate preds before saving model + model.eval() + pred_before_saving = model(x) + + # save model + new_weights_path = os.path.join(save_dir, 'save_test.ckpt') + trainer.save_checkpoint(new_weights_path) + + # load new model + tags_path = exp.get_data_path(exp.name, exp.version) + tags_path = os.path.join(tags_path, 'meta_tags.csv') + model_2 = LightningTestModel.load_from_metrics(weights_path=new_weights_path, tags_csv=tags_path, on_gpu=False) + model_2.eval() + + # make prediction + # assert that both predictions are the same + new_pred = model_2(x) + assert torch.all(torch.eq(pred_before_saving, new_pred)).item() == 1 + + clear_save_dir() + + + + +def test_model_freeze_unfreeze(): + hparams = get_hparams() + model = LightningTestModel(hparams) + + model.freeze() + model.unfreeze() + + +def test_amp_gpu_ddp_slurm_managed(): + """ + Make sure DDP + AMP work + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + + # simulate setting slurm flags + os.environ['MASTER_PORT'] = str(np.random.randint(12000, 19000, 1)[0]) + os.environ['SLURM_LOCALID'] = str(0) + + hparams = get_hparams() + model = LightningTestModel(hparams) + + trainer_options = dict( + progress_bar=True, + max_nb_epochs=1, + gpus=[0], + distributed_backend='ddp', + use_amp=True + ) + + save_dir = init_save_dir() + + # exp file to get meta + exp = get_exp(False) + exp.argparse(hparams) + exp.save() + + # exp file to get weights + checkpoint = ModelCheckpoint(save_dir) + + # add these to the trainer options + trainer_options['checkpoint_callback'] = checkpoint + trainer_options['experiment'] = exp + + # fit model + trainer = Trainer(**trainer_options) + trainer.is_slurm_managing_tasks = True + result = trainer.fit(model) + + # correct result and ok accuracy + assert result == 1, 'amp + ddp model failed to complete' + + # test root model address + assert trainer.resolve_root_node_address('abc') == 'abc' + assert trainer.resolve_root_node_address('abc[23]') == 'abc23' + assert trainer.resolve_root_node_address('abc[23-24]') == 'abc23' + assert trainer.resolve_root_node_address('abc[23-24, 45-40, 40]') == 'abc23' + + # test model loading with a map_location + map_location = 'cuda:1' + pretrained_model = load_model(exp, save_dir, True, map_location) + + # test model preds + run_prediction(model.test_dataloader, pretrained_model) + + if trainer.use_ddp: + # on hpc this would work fine... but need to hack it for the purpose of the test + trainer.model = pretrained_model + trainer.optimizers = pretrained_model.configure_optimizers() + + # test HPC loading / saving + trainer.hpc_save(save_dir, exp) + trainer.hpc_load(save_dir, on_gpu=True) + + # test freeze on gpu + model.freeze() + model.unfreeze() + + clear_save_dir() + + +def test_early_stopping_cpu_model(): + """ + Test each of the trainer options + :return: + """ + + stopping = EarlyStopping() + trainer_options = dict( + early_stop_callback=stopping, + gradient_clip=1.0, + overfit_pct=0.20, + track_grad_norm=2, + print_nan_grads=True, + progress_bar=False, + experiment=get_exp(), + train_percent_check=0.1, + val_percent_check=0.1 + ) + + model, hparams = get_model() + run_gpu_model_test(trainer_options, model, hparams, on_gpu=False) + + # test freeze on cpu + model.freeze() + model.unfreeze() + + +def test_cpu_model_with_amp(): + """ + Make sure model trains on CPU + :return: + """ + + trainer_options = dict( + progress_bar=False, + experiment=get_exp(), + max_nb_epochs=1, + train_percent_check=0.4, + val_percent_check=0.4, + use_amp=True + ) + + model, hparams = get_model() + + with pytest.raises((MisconfigurationException, ModuleNotFoundError)): + run_gpu_model_test(trainer_options, model, hparams, on_gpu=False) + + +def test_cpu_model(): + """ + Make sure model trains on CPU + :return: + """ + + trainer_options = dict( + progress_bar=False, + experiment=get_exp(), + max_nb_epochs=1, + train_percent_check=0.4, + val_percent_check=0.4 + ) + + model, hparams = get_model() + + run_gpu_model_test(trainer_options, model, hparams, on_gpu=False) + + +def test_all_features_cpu_model(): + """ + Test each of the trainer options + :return: + """ + + trainer_options = dict( + gradient_clip=1.0, + overfit_pct=0.20, + track_grad_norm=2, + print_nan_grads=True, + progress_bar=False, + experiment=get_exp(), + max_nb_epochs=1, + train_percent_check=0.4, + val_percent_check=0.4 + ) + + model, hparams = get_model() + run_gpu_model_test(trainer_options, model, hparams, on_gpu=False) + + +def test_single_gpu_model(): + """ + Make sure single GPU works (DP mode) + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_single_gpu_model cannot run. Rerun on a GPU node to run this test') + return + model, hparams = get_model() + + trainer_options = dict( + progress_bar=False, + max_nb_epochs=1, + train_percent_check=0.1, + val_percent_check=0.1, + gpus=[0] + ) + + run_gpu_model_test(trainer_options, model, hparams) + + +def test_multi_gpu_model_dp(): + """ + Make sure DP works + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_multi_gpu_model_dp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_multi_gpu_model_dp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + model, hparams = get_model() + trainer_options = dict( + progress_bar=False, + max_nb_epochs=1, + train_percent_check=0.1, + val_percent_check=0.1, + gpus='-1' + ) + + run_gpu_model_test(trainer_options, model, hparams) + + # test memory helper functions + memory.get_gpu_memory_map() + + +def test_amp_gpu_dp(): + """ + Make sure DP + AMP work + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_amp_gpu_dp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_amp_gpu_dp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + model, hparams = get_model() + trainer_options = dict( + max_nb_epochs=1, + gpus='0, 1', # test init with gpu string + distributed_backend='dp', + use_amp=True + ) + with pytest.raises(MisconfigurationException): + run_gpu_model_test(trainer_options, model, hparams) + + +def test_multi_gpu_model_ddp(): + """ + Make sure DDP works + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_multi_gpu_model_ddp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_multi_gpu_model_ddp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + + os.environ['MASTER_PORT'] = str(np.random.randint(12000, 19000, 1)[0]) + model, hparams = get_model() + trainer_options = dict( + progress_bar=False, + max_nb_epochs=1, + train_percent_check=0.4, + val_percent_check=0.2, + gpus=[0, 1], + distributed_backend='ddp' + ) + + run_gpu_model_test(trainer_options, model, hparams) + + +def test_amp_gpu_ddp(): + """ + Make sure DDP + AMP work + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + + os.environ['MASTER_PORT'] = str(np.random.randint(12000, 19000, 1)[0]) + + hparams = get_hparams() + model = LightningTestModel(hparams) + + trainer_options = dict( + progress_bar=True, + max_nb_epochs=1, + gpus=[0, 1], + distributed_backend='ddp', + use_amp=True + ) + + run_gpu_model_test(trainer_options, model, hparams) + + +def test_ddp_sampler_error(): + """ + Make sure DDP + AMP work + :return: + """ + if not torch.cuda.is_available(): + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a GPU node to run this test') + return + if not torch.cuda.device_count() > 1: + warnings.warn('test_amp_gpu_ddp cannot run. Rerun on a node with 2+ GPUs to run this test') + return + + os.environ['MASTER_PORT'] = str(np.random.randint(12000, 19000, 1)[0]) + + hparams = get_hparams() + model = LightningTestModel(hparams, force_remove_distributed_sampler=True) + + exp = get_exp(True) + exp.save() + + trainer = Trainer( + experiment=exp, + progress_bar=False, + max_nb_epochs=1, + gpus=[0, 1], + distributed_backend='ddp', + use_amp=True + ) + + with pytest.raises(MisconfigurationException): + trainer.get_dataloaders(model) + + clear_save_dir() + + +# ------------------------------------------------------------------------ +# UTILS +# ------------------------------------------------------------------------ +def run_gpu_model_test(trainer_options, model, hparams, on_gpu=True): + save_dir = init_save_dir() + + # exp file to get meta + exp = get_exp(False) + exp.argparse(hparams) + exp.save() + + # exp file to get weights + checkpoint = ModelCheckpoint(save_dir) + + # add these to the trainer options + trainer_options['checkpoint_callback'] = checkpoint + trainer_options['experiment'] = exp + + # fit model + trainer = Trainer(**trainer_options) + result = trainer.fit(model) + + # correct result and ok accuracy + assert result == 1, 'amp + ddp model failed to complete' + + # test model loading + pretrained_model = load_model(exp, save_dir, on_gpu) + + # test model preds + run_prediction(model.test_dataloader, pretrained_model) + + if trainer.use_ddp: + # on hpc this would work fine... but need to hack it for the purpose of the test + trainer.model = pretrained_model + trainer.optimizers = pretrained_model.configure_optimizers() + + # test HPC loading / saving + trainer.hpc_save(save_dir, exp) + trainer.hpc_load(save_dir, on_gpu=on_gpu) + + clear_save_dir() + + +def get_hparams(continue_training=False, hpc_exp_number=0): + root_dir = os.path.dirname(os.path.realpath(__file__)) + + args = { + 'drop_prob': 0.2, + 'batch_size': 32, + 'in_features': 28*28, + 'learning_rate': 0.001*8, + 'optimizer_name': 'adam', + 'data_root': os.path.join(root_dir, 'mnist'), + 'out_features': 10, + 'hidden_dim': 1000} + + if continue_training: + args['test_tube_do_checkpoint_load'] = True + args['hpc_exp_number'] = hpc_exp_number + + hparams = Namespace(**args) + return hparams + + +def get_model(): + # set up model with these hyperparams + hparams = get_hparams() + model = LightningTemplateModel(hparams) + + return model, hparams + + +def get_exp(debug=True): + # set up exp object without actually saving logs + root_dir = os.path.dirname(os.path.realpath(__file__)) + exp = Experiment(debug=debug, save_dir=root_dir, name='tests_tt_dir') + return exp + + +def init_save_dir(): + root_dir = os.path.dirname(os.path.realpath(__file__)) + save_dir = os.path.join(root_dir, 'save_dir') + + if os.path.exists(save_dir): + shutil.rmtree(save_dir) + + os.makedirs(save_dir, exist_ok=True) + + return save_dir + + +def clear_save_dir(): + root_dir = os.path.dirname(os.path.realpath(__file__)) + save_dir = os.path.join(root_dir, 'save_dir') + if os.path.exists(save_dir): + shutil.rmtree(save_dir) + + +def load_model(exp, save_dir, on_gpu, map_location=None): + + # load trained model + tags_path = exp.get_data_path(exp.name, exp.version) + tags_path = os.path.join(tags_path, 'meta_tags.csv') + + checkpoints = [x for x in os.listdir(save_dir) if '.ckpt' in x] + weights_dir = os.path.join(save_dir, checkpoints[0]) + + trained_model = LightningTemplateModel.load_from_metrics(weights_path=weights_dir, + tags_csv=tags_path, + on_gpu=on_gpu, + map_location=map_location) + + assert trained_model is not None, 'loading model failed' + + return trained_model + + +def run_prediction(dataloader, trained_model): + # run prediction on 1 batch + for batch in dataloader: + break + + x, y = batch + x = x.view(x.size(0), -1) + + y_hat = trained_model(x) + + # acc + labels_hat = torch.argmax(y_hat, dim=1) + val_acc = torch.sum(y == labels_hat).item() / (len(y) * 1.0) + val_acc = torch.tensor(val_acc) + val_acc = val_acc.item() + + print(val_acc) + + assert val_acc > 0.50, f'this model is expected to get > 0.50 in test set (it got {val_acc})' + + +def assert_ok_acc(trainer): + # this model should get 0.80+ acc + acc = trainer.tng_tqdm_dic['val_acc'] + assert acc > 0.50, f'model failed to get expected 0.50 validation accuracy. Got: {acc}' + + +if __name__ == '__main__': + pytest.main([__file__])