Support target classpath resolution for all ModelPT subclasses
State: closed | Pull request: yes

# Changelog
- If `target` exists on its own inside a config, will attempt to resolve the class path inside target
- If target classpath can be resolved, attempts to load model by calling the constructor of the target class
- If model is successfully loaded via this target classpath, then return this model
- If model fails to load via the target classpath, log under debug scope and fallback to current `cls` as the target class.
- Adds a test case to cover this functionality

# Example usage
With this, given any .nemo file, it can be loaded as 

```python
from nemo.core import ModelPT
model = ModelPT.restore_from("<path/to/any/.nemo/file>")  # will be subclass of ModelPT if loads successfully
```

# Restrictions
While this works for all `restore_from` calls, it is not the case for `from_pretrained`. All domains have their own way of populating the list of pretrained models - and therefore calling `from_pretrained` from ModelPT will fail since ModelPT has no information of the subclass pretrained models.

Signed-off-by: smajumdar <titu1994@gmail.com>

>>"While this works for all restore_from calls, it is not the case for from_pretrained. All domains have their own way of populating the list of pretrained models - and therefore calling from_pretrained from ModelPT will fail since ModelPT has no information of the subclass pretrained models."

- I don't understand this. "from_pretrained" just calls "restore_from" once .nemo file is downloaded.

> > > "While this works for all restore_from calls, it is not the case for from_pretrained. All domains have their own way of populating the list of pretrained models - and therefore calling from_pretrained from ModelPT will fail since ModelPT has no information of the subclass pretrained models."
> 
> * I don't understand this. "from_pretrained" just calls "restore_from" once .nemo file is downloaded.

> I don't understand this. "from_pretrained" just calls "restore_from" once .nemo file is downloaded.
from_pretrained first loads up the list of pretrained models with download links from the `list_available_models` method.

If its called from `EncDecCTCModel` (say) - then it will load up the correct set of model `PretrainedInfo`. However, if its called from `ModelPT` - then `ModelPT.restore_from('stt_en_citrinet_256')` will fail - since ModelPT has an empty implementation of `list_available_models`. So it raises an error saying the name `stt_en_citrinet_256` is not valid.

An easy solution is to perform a DFS inside ModelPT for all possible subclass implementations of `list_available_models` - and populate a set of all pretraind model info. However, this is up for debate if core should do such a task. For an example of such a population, look to ASRModel's `list_available_models` implementation - it populates its list from all of its own subclasses. ModelPT would just do this, but at one level higher up the chain.

> > I don't understand this. "from_pretrained" just calls "restore_from" once .nemo file is downloaded.
> > from_pretrained first loads up the list of pretrained models with download links from the `list_available_models` method.
> 
> If its called from `EncDecCTCModel` (say) - then it will load up the correct set of model `PretrainedInfo`. However, if its called from `ModelPT` - then `ModelPT.restore_from('stt_en_citrinet_256')` will fail - since ModelPT has an empty implementation of `list_available_models`. So it raises an error saying the name `stt_en_citrinet_256` is not valid.
> 
> An easy solution is to perform a DFS inside ModelPT for all possible subclass implementations of `list_available_models` - and populate a set of all pretraind model info. However, this is up for debate if core should do such a task. For an example of such a population, look to ASRModel's `list_available_models` implementation - it populates its list from all of its own subclasses. ModelPT would just do this, but at one level higher up the chain.

makes sense

how about, inside ```nemo.core.classes.common.FileIO.to_config_file``` we check if "target" is present for the model and if not, we add it there?

> how about, inside nemo.core.classes.common.FileIO.to_config_file we check if "target" is present for the model and if not, we add it there?

Probably not a good idea. ModelPT injects `target` which is safe. since all other config modules inside it are "nearly" independent and called explicitly by the constructor of the subclass of ModelPT.

On the other hand, `FileIO.to_config_file` is called under many different cases - simple modules like encoder or decoder or spec_augment all can call both `from_config_file` and `to_config_file`. Injecting parameters there would make the constructors of such classes have issues.

>>"ModelPT injects target which is safe."

-- where? 

Basically, I'm saying that if we do ```.save_to``` with this version, then ```ModelPT.restore_from``` should work for the newly generated file

Inside the constructor - https://github.com/NVIDIA/NeMo/blob/main/nemo/core/classes/modelPT.py#L99-L103

I can add a save-restore-save test, but for some reason I think we have that already. The target injection has been there since b1 so it should be safe if the model tests regarding Citrinet and rest are passing.