Repository: Lightning-AI/pytorch-lightning

Issue:
Exception when trainer.gpus are passed as a list through hydra (omegaconf)
## 🐛 Bug
When we pass the `gpus` flag for `Trainer` using `hydra` (which in turn uses `Omegaconf`) it is passed as `omegaconf.listconfig.ListConfig`. However, internally `Trainer` only checks for `int`, `str`, `list`. This throws an exception. 

#### Code sample
```
#!/usr/bin/env python

import warnings
warnings.filterwarnings("ignore")

import os

import torch
from torch import nn
from torch.utils.data import DataLoader, random_split
from torchvision.datasets import MNIST
from torchvision import datasets, transforms
from torch.nn import functional as F
from torch import optim

from pytorch_lightning.core.lightning import LightningModule
from pytorch_lightning import Trainer
from omegaconf import OmegaConf

class LitMNIST(LightningModule):
  def __init__(self):
      super().__init__()
      self.layer_1 = torch.nn.Linear(28 * 28, 128)
      self.layer_2 = torch.nn.Linear(128, 256)
      self.layer_3 = torch.nn.Linear(256, 10)  
      
  def prepare_data(self):
    transform=transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
    # transform
    mnist_train = MNIST(os.getcwd(), train=True, download=False, transform=transforms.ToTensor())
    
    # train/val split
    mnist_train, mnist_val = random_split(mnist_train, [55000, 5000])

    # assign to use in dataloaders
    self.train_dataset = mnist_train
    self.val_dataset = mnist_val
    
  def forward(self, x):
      batch_size, channels, width, height = x.size()
      x = x.view(batch_size, -1)
      x = self.layer_1(x)
      x = torch.relu(x)
      x = self.layer_2(x)
      x = torch.relu(x)
      x = self.layer_3(x)
      x = torch.log_softmax(x, dim=1)
      return x    
    
  def training_step(self, batch, batch_idx):
      x, y = batch
      logits = self(x)
      loss = F.nll_loss(logits, y)

      # add logging
      logs = {'loss': loss}
      return {'loss': loss, 'log': logs}    
    
  def validation_step(self, batch, batch_idx):
    x, y = batch
    logits = self(x)
    loss = F.nll_loss(logits, y)
    return {'val_loss': loss}    

  def train_dataloader(self):
    return DataLoader(self.train_dataset, batch_size=64)
  
  def val_dataloader(self):
    return DataLoader(self.val_dataset, batch_size=64) 
  
  def configure_optimizers(self):
    return optim.Adam(self.parameters(), lr=1e-3)    

if __name__=='__main__':
#   trainer_params = OmegaConf.create({'max_epochs': 1, 'overfit_pct': 0.01, 'gpus': 1}) # this works
#   trainer_params = OmegaConf.create({'max_epochs': 1, 'overfit_pct': 0.01, 'gpus': [1]}) # this throws an exception about gpus not being int, str, or list(int)
  model = LitMNIST()
  trainer = Trainer(**trainer_params)
  trainer.fit(model)
```


Discussion:
well it is OmegaConf issue
```
trainer_params = OmegaConf.create({'max_epochs': 1, 'overfit_pct': 0.01, 'gpus': [1]})
type(trainer_params.gpus)
Out[1]: omegaconf.listconfig.ListConfig
isinstance(trainer_params.gpus, list)
Out[2]: False
```
in fact, the solution shall be that `ListConfig` is inherited from `list`, so no change needed for PL
@omry ^^ @williamFalcon 
cc: @PyTorchLightning/core-contributors 
@Borda : 

Inheriting from primitive containers is not recommended.
Python is making a ton of assumptions about list and dict that would break if the objects are not actually what it expects.
Here is a [post](https://treyhunner.com/2019/04/why-you-shouldnt-inherit-from-list-and-dict-in-python/) by someone talking about some of the problems.

As of OmegaConf 2.0 ListConfig is [implementing MutableSequence](https://github.com/omry/omegaconf/blob/master/omegaconf/listconfig.py#L38). and DictConfig is [implementing MutableMapping](https://github.com/omry/omegaconf/blob/master/omegaconf/dictconfig.py#L47) to make it easier to identify them in a way that works for lists and dicts as well.

Additionally:
OmegaConf is very low level, making such changes there is a huge deal. This kind of change will require a major version. (not happening in the next 6 months, and probably not happening at all for the reasons I outlined above).
```python
In [1]: from omegaconf import OmegaConf
In [2]: trainer_params = OmegaConf.create({'max_epochs': 1, 'overfit_pct': 0.01, 'gpus': [1]})
In [3]: isinstance(trainer_params.gpus, list)
Out[3]: False
In [4]: from typing import MutableSequence
In [5]: isinstance(trainer_params.gpus, MutableSequence)
Out[5]: True
In [6]: isinstance([1,2,3], MutableSequence)
Out[6]: True
```

Please note that this is only working with OmegaConf 2.0, which means this is only working with Hydra 1.0.0rc1 or newer (which is still a release candidate, most Hydra users are still using the stable version).
However, it looks like the PL support for Hydra is already assuming 1.0 so we might as well continue to assume it.
```
In[8]: from typing import List
In[9]: isinstance(trainer_params.gpus, List)
Out[9]: False
```
ok, we can replace check of `list` to `MutableSequence`

Resolving patch:
diff --git a/pytorch_lightning/core/saving.py b/pytorch_lightning/core/saving.py
--- a/pytorch_lightning/core/saving.py
+++ b/pytorch_lightning/core/saving.py
@@ -6,20 +6,18 @@
 import torch
 import yaml
 from argparse import Namespace
-from typing import Union, Dict, Any, Optional, Callable
+from typing import Union, Dict, Any, Optional, Callable, MutableMapping
 
 from pytorch_lightning import _logger as log
 from pytorch_lightning.utilities import rank_zero_warn, AttributeDict
 from pytorch_lightning.utilities.cloud_io import load as pl_load
 
 PRIMITIVE_TYPES = (bool, int, float, str)
-ALLOWED_CONFIG_TYPES = (AttributeDict, dict, Namespace)
+ALLOWED_CONFIG_TYPES = (AttributeDict, MutableMapping, Namespace)
 try:
     from omegaconf import Container
 except ImportError:
     Container = None
-else:
-    ALLOWED_CONFIG_TYPES = ALLOWED_CONFIG_TYPES + (Container, )
 
 # the older shall be on the top
 CHECKPOINT_PAST_HPARAMS_KEYS = (
diff --git a/pytorch_lightning/trainer/distrib_parts.py b/pytorch_lightning/trainer/distrib_parts.py
--- a/pytorch_lightning/trainer/distrib_parts.py
+++ b/pytorch_lightning/trainer/distrib_parts.py
@@ -10,7 +10,7 @@
 import time
 import random
 import torch
-from typing import Union, Callable, Any, List, Optional, Tuple
+from typing import Union, Callable, Any, List, Optional, Tuple, MutableSequence
 
 from pytorch_lightning.core.lightning import LightningModule
 from pytorch_lightning import _logger as log
@@ -356,14 +356,14 @@ def check_gpus_data_type(gpus: Any) -> None:
     Args:
         gpus: parameter as passed to the Trainer
     """
-    if gpus is not None and (not isinstance(gpus, (int, str, list)) or isinstance(gpus, bool)):
-        raise MisconfigurationException("GPUs must be int, string or list of ints or None.")
+    if gpus is not None and (not isinstance(gpus, (int, str, MutableSequence)) or isinstance(gpus, bool)):
+        raise MisconfigurationException("GPUs must be int, string or sequence of ints or None.")
 
 
 def normalize_parse_gpu_input_to_list(gpus: Union[int, List[int]]) -> Optional[List[int]]:
     assert gpus is not None
-    if isinstance(gpus, list):
-        return gpus
+    if isinstance(gpus, MutableSequence):
+        return list(gpus)
 
     # must be an int
     if not gpus:  # gpus==0


Test patch:
diff --git a/tests/trainer/test_trainer.py b/tests/trainer/test_trainer.py
--- a/tests/trainer/test_trainer.py
+++ b/tests/trainer/test_trainer.py
@@ -10,6 +10,7 @@
 import cloudpickle
 import pytest
 import torch
+from omegaconf import OmegaConf
 
 import tests.base.utils as tutils
 from pytorch_lightning import Callback, LightningModule, Trainer
@@ -905,6 +906,15 @@ def __init__(self, **kwargs):
         TrainerSubclass(abcdefg='unknown_arg')
 
 
+@pytest.mark.parametrize('trainer_params', [
+    OmegaConf.create({'max_epochs': 1, 'gpus': 1}),
+    OmegaConf.create({'max_epochs': 1, 'gpus': [0]}),
+])
+@pytest.mark.skipif(not torch.cuda.is_available(), reason="test requires GPU machine")
+def test_trainer_omegaconf(trainer_params):
+    Trainer(**trainer_params)
+
+
 def test_trainer_pickle(tmpdir):
     trainer = Trainer(
         max_epochs=1,