Introduce code from Rust monorepo. This is a mess

This commit is contained in:
Brandon Thomas
2022-02-28 05:43:54 -05:00
parent 921fb49f9a
commit ba326bfcfd
1058 changed files with 192192 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.utils.app_state import AppState
from nemo.utils.nemo_logging import Logger as _Logger
from nemo.utils.nemo_logging import LogMode as logging_mode
from nemo.utils.lightning_logger_patch import add_memory_handlers_to_pl_logger
logging = _Logger()
add_memory_handlers_to_pl_logger()
+386
View File
@@ -0,0 +1,386 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from threading import Lock
from typing import Dict, Optional
from nemo.utils.metaclasses import Singleton
@dataclass()
class ModelMetadataRegistry:
guid: str
gidx: int
restoration_path: Optional[str] = None
class AppState(metaclass=Singleton):
def __init__(self):
# method call lock
self.__lock = Lock()
# TODO: should we store global config in hydra_runner?
self._app_cfg = None
# World info
self._device_id = None
self._local_rank = None
self._global_rank = None
self._model_parallel_rank = None
self._data_parallel_rank = None
self._world_size = None
self._model_parallel_size = None
self._model_parallel_group = None
self._data_parallel_size = None
self._data_parallel_group = None
self._random_seed = None
# Logging info
self._log_dir = None
self._exp_dir = None
self._name = None
self._checkpoint_name = None
self._version = None
self._create_checkpoint_callback = None
self._checkpoint_callback_params = None
# Save and Restore (.nemo)
self._tmpdir_name = None
self._model_config_yaml = "model_config.yaml"
self._model_weights_ckpt = "model_weights.ckpt"
self._model_restore_path = None
self._all_model_restore_paths = []
self._model_guid_map = {} # type: Dict[str, ModelMetadataRegistry]
@property
def device_id(self):
""" Property returns the device_id
Returns:
device_id
"""
return self._device_id
@device_id.setter
def device_id(self, id):
""" Property sets the device_id.
Args:
size (int): The device id.
"""
self._device_id = id
@property
def world_size(self):
""" Property returns the total number of GPUs.
Returns:
Total number of GPUs.
"""
return self._world_size
@world_size.setter
def world_size(self, size):
""" Property sets the total number of GPUs.
Args:
size (int): Total number of GPUs.
"""
self._world_size = size
@property
def model_parallel_size(self):
""" Property returns the number of GPUs in each model parallel group.
Returns:
Number of GPUs in each model parallel group.
"""
return self._model_parallel_size
@model_parallel_size.setter
def model_parallel_size(self, size):
""" Property sets the number of GPUs in each model parallel group.
Args:
size (int): Number of GPUs in each model parallel group.
"""
self._model_parallel_size = size
@property
def data_parallel_size(self):
""" Property returns the number of GPUs in each data parallel group.
Returns:
Number of GPUs in each data parallel group.
"""
return self._data_parallel_size
@data_parallel_size.setter
def data_parallel_size(self, size):
""" Property sets the number of GPUs in each data parallel group.
Args:
size (int): Number of GPUs in each data parallel group.
"""
self._data_parallel_size = size
@property
def local_rank(self):
""" Property returns the local rank.
Returns:
Local rank.
"""
return self._local_rank
@local_rank.setter
def local_rank(self, rank):
""" Property sets the local rank.
Args:
rank (int): Local rank.
"""
self._local_rank = rank
@property
def global_rank(self):
""" Property returns the global rank.
Returns:
Global rank.
"""
return self._global_rank
@global_rank.setter
def global_rank(self, rank):
""" Property sets the global rank.
Args:
rank (int): Global rank.
"""
self._global_rank = rank
@property
def model_parallel_rank(self):
""" Property returns the model parallel rank.
Returns:
Model parallel rank.
"""
return self._model_parallel_rank
@model_parallel_rank.setter
def model_parallel_rank(self, rank):
""" Property sets the model parallel rank.
Args:
rank (int): Model parallel rank.
"""
self._model_parallel_rank = rank
@property
def model_parallel_group(self):
""" Property returns the model parallel group.
Returns:
Model parallel group.
"""
return self._model_parallel_group
@model_parallel_group.setter
def model_parallel_group(self, group):
""" Property sets the model parallel group.
Args:
group: Model parallel group.
"""
self._model_parallel_group = group
@property
def data_parallel_rank(self):
""" Property returns the data parallel rank.
Returns:
Data parallel rank.
"""
return self._data_parallel_rank
@data_parallel_rank.setter
def data_parallel_rank(self, rank):
""" Property sets the data parallel rank.
Args:
rank (int): Data parallel rank.
"""
self._data_parallel_rank = rank
@property
def data_parallel_group(self):
""" Property returns the data parallel group.
Returns:
Data parallel group.
"""
return self._data_parallel_group
@data_parallel_group.setter
def data_parallel_group(self, group):
""" Property sets the data parallel group.
Args:
group: Data parallel group.
"""
self._data_parallel_group = group
@property
def random_seed(self):
""" Property returns the random seed.
Returns:
Random seed.
"""
return self._random_seed
@random_seed.setter
def random_seed(self, seed):
""" Property sets the random seed.
Args:
seed (int): Random seed.
"""
self._random_seed = seed
@property
def log_dir(self):
"""Returns the log_dir set by exp_manager.
"""
return self._log_dir
@log_dir.setter
def log_dir(self, dir):
"""Sets the log_dir property.
Args:
dir (str): Log_dir set by exp_manager.
"""
self._log_dir = dir
@property
def exp_dir(self):
"""Returns the exp_dir set by exp_manager.
"""
return self._exp_dir
@exp_dir.setter
def exp_dir(self, dir):
"""Sets the log_dir property.
Args:
dir (str): Log_dir set by exp_manager.
"""
self._exp_dir = dir
@property
def name(self):
"""Returns the name set by exp_manager.
"""
return self._name
@name.setter
def name(self, name):
"""Sets the name property.
Args:
dir (str): name set by exp_manager.
"""
self._name = name
@property
def checkpoint_name(self):
"""Returns the name set by exp_manager.
"""
return self._checkpoint_name
@checkpoint_name.setter
def checkpoint_name(self, name):
"""Sets the name property.
Args:
dir (str): name set by exp_manager.
"""
self._checkpoint_name = name
@property
def version(self):
"""Returns the version set by exp_manager.
"""
return self._version
@version.setter
def version(self, version):
"""Sets the version property.
Args:
dir (str): version set by exp_manager.
"""
self._version = version
@property
def create_checkpoint_callback(self):
"""Returns the create_checkpoint_callback set by exp_manager.
"""
return self._create_checkpoint_callback
@create_checkpoint_callback.setter
def create_checkpoint_callback(self, create_checkpoint_callback):
"""Sets the create_checkpoint_callback property.
Args:
dir (bool): create_checkpoint_callback set by exp_manager.
"""
self._create_checkpoint_callback = create_checkpoint_callback
@property
def checkpoint_callback_params(self):
"""Returns the version set by exp_manager.
"""
return self._checkpoint_callback_params
@checkpoint_callback_params.setter
def checkpoint_callback_params(self, params):
"""Sets the name property.
Args:
params (dict): checkpoint_callback_params set by exp_manager.
"""
self._checkpoint_callback_params = params
@property
def model_config_yaml(self):
return self._model_config_yaml
@property
def model_weights_ckpt(self):
return self._model_weights_ckpt
@property
def model_restore_path(self):
restore_path = self._all_model_restore_paths[-1] if len(self._all_model_restore_paths) > 0 else None
return restore_path
@model_restore_path.setter
def model_restore_path(self, path):
with self.__lock:
self._model_restore_path = path
self._all_model_restore_paths.append(path)
def register_model_guid(self, guid: str, restoration_path: Optional[str] = None):
# Maps a guid to its restore path (None or last absolute path)
with self.__lock:
if guid in self._model_guid_map:
idx = self._model_guid_map[guid].gidx
else:
idx = len(self._model_guid_map)
self._model_guid_map[guid] = ModelMetadataRegistry(guid, idx, restoration_path=restoration_path)
def reset_model_guid_registry(self):
# Reset the guid mapping
with self.__lock:
self._model_guid_map.clear()
def get_model_metadata_from_guid(self, guid) -> ModelMetadataRegistry:
# Returns the global model idx and restoration path
metadata = self._model_guid_map[guid]
return metadata
+132
View File
@@ -0,0 +1,132 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from argparse import ArgumentParser
from typing import Any, Dict, List, Optional, Union
def add_optimizer_args(
parent_parser: ArgumentParser,
optimizer: str = 'adam',
default_lr: float = None,
default_opt_args: Optional[Union[Dict[str, Any], List[str]]] = None,
) -> ArgumentParser:
"""Extends existing argparse with support for optimizers.
# Example of adding optimizer args to command line :
python train_script.py ... --optimizer "novograd" --lr 0.01 \
--opt_args betas=0.95,0.5 weight_decay=0.001
Args:
parent_parser (ArgumentParser): Custom CLI parser that will be extended.
optimizer (str): Default optimizer required.
default_lr (float): Default learning rate that should be overriden during training.
default_opt_args (list(str)): List of overriding arguments for the instantiated optimizer.
Returns:
ArgumentParser: Parser extended by Optimizers arguments.
"""
if default_opt_args is None:
default_opt_args = []
parser = ArgumentParser(parents=[parent_parser], add_help=True, conflict_handler='resolve')
parser.add_argument('--optimizer', type=str, default=optimizer, help='Name of the optimizer. Defaults to Adam.')
parser.add_argument('--lr', type=float, default=default_lr, help='Learning rate of the optimizer.')
parser.add_argument(
'--opt_args',
default=default_opt_args,
nargs='+',
type=str,
help='Overriding arguments for the optimizer. \n Must follow the pattern : \n name=value separated by spaces.'
'Example: --opt_args weight_decay=0.001 eps=1e-8 betas=0.9,0.999',
)
return parser
def add_scheduler_args(parent_parser: ArgumentParser) -> ArgumentParser:
"""Extends existing argparse with default LR scheduler args.
Args:
parent_parser (ArgumentParser): Custom CLI parser that will be extended.
Returns:
ArgumentParser: Parser extended by LR Scheduler arguments.
"""
parser = ArgumentParser(parents=[parent_parser], add_help=False, conflict_handler='resolve')
parser.add_argument("--warmup_steps", type=int, required=False, default=None, help="Number of warmup steps")
parser.add_argument(
"--warmup_ratio",
type=float,
required=False,
default=None,
help="Number of warmup steps as a percentage of total training steps",
)
parser.add_argument("--hold_steps", type=int, required=False, default=None, help="Number of hold LR steps")
parser.add_argument(
"--hold_ratio",
type=float,
required=False,
default=None,
help="Number of hold LR steps as a percentage of total training steps",
)
parser.add_argument("--min_lr", type=float, required=False, default=0.0, help="Minimum learning rate")
parser.add_argument(
"--last_epoch", type=int, required=False, default=-1, help="Last epoch id. -1 indicates training from scratch"
)
return parser
def add_asr_args(parent_parser: ArgumentParser) -> ArgumentParser:
"""Extends existing argparse with default ASR collection args.
Args:
parent_parser (ArgumentParser): Custom CLI parser that will be extended.
Returns:
ArgumentParser: Parser extended by NeMo ASR Collection arguments.
"""
parser = ArgumentParser(parents=[parent_parser], add_help=False, conflict_handler='resolve')
parser.add_argument("--asr_model", type=str, required=True, default="bad_quartznet15x5.yaml", help="")
parser.add_argument("--train_dataset", type=str, required=True, default=None, help="training dataset path")
parser.add_argument("--eval_dataset", type=str, required=True, help="evaluation dataset path")
return parser
def add_nlp_args(parent_parser: ArgumentParser) -> ArgumentParser:
"""Extends existing argparse with default NLP collection args.
Args:
parent_parser (ArgumentParser): Custom CLI parser that will be extended.
Returns:
ArgumentParser: Parser extended by NeMo NLP Collection arguments.
"""
parser = ArgumentParser(parents=[parent_parser], add_help=False, conflict_handler='resolve')
parser.add_argument(
"--data_dir", type=str, required=False, help="data directory to training or/and evaluation dataset"
)
parser.add_argument(
"--config_file", type=str, required=False, default=None, help="Huggingface model configuration file"
)
parser.add_argument(
"--pretrained_model_name", default='bert-base-uncased', type=str, required=False, help="pretrained model name"
)
parser.add_argument(
"--tokenizer_name", default='nemobert', type=str, choices=['sentencepiece', 'nemobert'], help="Tokenizer type"
)
parser.add_argument("--tokenizer_model", default=None, type=str, help="Tokenizer file for sentence piece")
parser.add_argument("--do_lower_case", action='store_true', required=False, help="lower case data")
return parser
+82
View File
@@ -0,0 +1,82 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from pathlib import Path
from time import sleep
import wget
from nemo.utils import logging
def maybe_download_from_cloud(url, filename, subfolder=None, cache_dir=None, refresh_cache=False) -> str:
"""
Helper function to download pre-trained weights from the cloud
Args:
url: (str) URL of storage
filename: (str) what to download. The request will be issued to url/filename
subfolder: (str) subfolder within cache_dir. The file will be stored in cache_dir/subfolder. Subfolder can
be empty
cache_dir: (str) a cache directory where to download. If not present, this function will attempt to create it.
If None (default), then it will be $HOME/.cache/torch/NeMo
refresh_cache: (bool) if True and cached file is present, it will delete it and re-fetch
Returns:
If successful - absolute local path to the downloaded file
else - empty string
"""
# try:
if cache_dir is None:
cache_location = Path.joinpath(Path.home(), ".cache/torch/NeMo")
else:
cache_location = cache_dir
if subfolder is not None:
destination = Path.joinpath(cache_location, subfolder)
else:
destination = cache_location
if not os.path.exists(destination):
os.makedirs(destination, exist_ok=True)
destination_file = Path.joinpath(destination, filename)
if os.path.exists(destination_file):
logging.info(f"Found existing object {destination_file}.")
if refresh_cache:
logging.info("Asked to refresh the cache.")
logging.info(f"Deleting file: {destination_file}")
os.remove(destination_file)
else:
logging.info(f"Re-using file from: {destination_file}")
return str(destination_file)
# download file
wget_uri = url + filename
logging.info(f"Downloading from: {wget_uri} to {str(destination_file)}")
# NGC links do not work everytime so we try and wait
i = 0
max_attempts = 3
while i < max_attempts:
i += 1
try:
wget.download(wget_uri, str(destination_file))
if os.path.exists(destination_file):
return destination_file
else:
return ""
except:
logging.info(f"Download from cloud failed. Attempt {i} of {max_attempts}")
sleep(0.05)
continue
raise ValueError("Not able to download url right now, please try again.")
+258
View File
@@ -0,0 +1,258 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import inspect
from dataclasses import is_dataclass
from typing import Dict, List, Optional
from omegaconf import DictConfig, OmegaConf, open_dict
from nemo.core.config.modelPT import NemoConfig
from nemo.utils import logging
def update_model_config(model_cls: NemoConfig, update_cfg: DictConfig, drop_missing_subconfigs: bool = True):
"""
Helper class that updates the default values of a ModelPT config class with the values
in a DictConfig that mirrors the structure of the config class.
Assumes the `update_cfg` is a DictConfig (either generated manually, via hydra or instantiated via yaml/model.cfg).
This update_cfg is then used to override the default values preset inside the ModelPT config class.
If `drop_missing_subconfigs` is set, the certain sub-configs of the ModelPT config class will be removed, iff
they are not found in the mirrored `update_cfg`. The following sub-configs are subject to potential removal:
- `train_ds`
- `validation_ds`
- `test_ds`
- `optim` + nested `sched`.
Args:
model_cls: A subclass of NemoConfig, that details in entirety all of the parameters that constitute
the NeMo Model.
update_cfg: A DictConfig that mirrors the structure of the NemoConfig data class. Used to update the
default values of the config class.
drop_missing_subconfigs: Bool which determins whether to drop certain sub-configs from the NemoConfig
class, if the corresponding sub-config is missing from `update_cfg`.
Returns:
A DictConfig with updated values that can be used to instantiate the NeMo Model along with supporting
infrastructure.
"""
if not (is_dataclass(model_cls) or isinstance(model_cls, DictConfig)):
raise ValueError("`model_cfg` must be a dataclass or a structured OmegaConf object")
if not isinstance(update_cfg, DictConfig):
update_cfg = OmegaConf.create(update_cfg)
if is_dataclass(model_cls):
model_cls = OmegaConf.structured(model_cls)
# Update optional configs
model_cls = _update_subconfig(
model_cls, update_cfg, subconfig_key='train_ds', drop_missing_subconfigs=drop_missing_subconfigs
)
model_cls = _update_subconfig(
model_cls, update_cfg, subconfig_key='validation_ds', drop_missing_subconfigs=drop_missing_subconfigs
)
model_cls = _update_subconfig(
model_cls, update_cfg, subconfig_key='test_ds', drop_missing_subconfigs=drop_missing_subconfigs
)
model_cls = _update_subconfig(
model_cls, update_cfg, subconfig_key='optim', drop_missing_subconfigs=drop_missing_subconfigs
)
# Add optim and sched additional keys to model cls
model_cls = _add_subconfig_keys(model_cls, update_cfg, subconfig_key='optim')
# Perform full merge of model config class and update config
# Remove ModelPT artifact `target`
if 'target' in update_cfg.model:
# Assume artifact from ModelPT and pop
if 'target' not in model_cls.model:
with open_dict(update_cfg.model):
update_cfg.model.pop('target')
model_cfg = OmegaConf.merge(model_cls, update_cfg)
return model_cfg
def _update_subconfig(
model_cfg: DictConfig, update_cfg: DictConfig, subconfig_key: str, drop_missing_subconfigs: bool
):
"""
Updates the NemoConfig DictConfig such that:
1) If the sub-config key exists in the `update_cfg`, but does not exist in ModelPT config:
- Add the sub-config from update_cfg to ModelPT config
2) If the sub-config key does not exist in `update_cfg`, but exists in ModelPT config:
- Remove the sub-config from the ModelPT config; iff the `drop_missing_subconfigs` flag is set.
Args:
model_cfg: A DictConfig instantiated from the NemoConfig subclass.
update_cfg: A DictConfig that mirrors the structure of `model_cfg`, used to update its default values.
subconfig_key: A str key used to check and update the sub-config.
drop_missing_subconfigs: A bool flag, whether to allow deletion of the NemoConfig sub-config,
if its mirror sub-config does not exist in the `update_cfg`.
Returns:
The updated DictConfig for the NemoConfig
"""
with open_dict(model_cfg.model):
# If update config has the key, but model cfg doesnt have the key
# Add the update cfg subconfig to the model cfg
if subconfig_key in update_cfg.model and subconfig_key not in model_cfg.model:
model_cfg.model[subconfig_key] = update_cfg.model[subconfig_key]
# If update config does not the key, but model cfg has the key
# Remove the model cfg subconfig in order to match layout of update cfg
if subconfig_key not in update_cfg.model and subconfig_key in model_cfg.model:
if drop_missing_subconfigs:
model_cfg.model.pop(subconfig_key)
return model_cfg
def _add_subconfig_keys(model_cfg: DictConfig, update_cfg: DictConfig, subconfig_key: str):
"""
For certain sub-configs, the default values specified by the NemoConfig class is insufficient.
In order to support every potential value in the merge between the `update_cfg`, it would require
explicit definition of all possible cases.
An example of such a case is Optimizers, and their equivalent Schedulers. All optimizers share a few basic
details - such as name and lr, but almost all require additional parameters - such as weight decay.
It is impractical to create a config for every single optimizer + every single scheduler combination.
In such a case, we perform a dual merge. The Optim and Sched Dataclass contain the bare minimum essential
components. The extra values are provided via update_cfg.
In order to enable the merge, we first need to update the update sub-config to incorporate the keys,
with dummy temporary values (merge update config with model config). This is done on a copy of the
update sub-config, as the actual override values might be overriden by the NemoConfig defaults.
Then we perform a merge of this temporary sub-config with the actual override config in a later step
(merge model_cfg with original update_cfg, done outside this function).
Args:
model_cfg: A DictConfig instantiated from the NemoConfig subclass.
update_cfg: A DictConfig that mirrors the structure of `model_cfg`, used to update its default values.
subconfig_key: A str key used to check and update the sub-config.
Returns:
A ModelPT DictConfig with additional keys added to the sub-config.
"""
with open_dict(model_cfg.model):
# Create copy of original model sub config
if subconfig_key in update_cfg.model:
if subconfig_key not in model_cfg.model:
# create the key as a placeholder
model_cfg.model[subconfig_key] = None
subconfig = copy.deepcopy(model_cfg.model[subconfig_key])
update_subconfig = copy.deepcopy(update_cfg.model[subconfig_key])
# Add the keys and update temporary values, will be updated during full merge
subconfig = OmegaConf.merge(update_subconfig, subconfig)
# Update sub config
model_cfg.model[subconfig_key] = subconfig
return model_cfg
def assert_dataclass_signature_match(
cls: 'class_type',
datacls: 'dataclass',
ignore_args: Optional[List[str]] = None,
remap_args: Optional[Dict[str, str]] = None,
):
"""
Analyses the signature of a provided class and its respective data class,
asserting that the dataclass signature matches the class __init__ signature.
Note:
This is not a value based check. This function only checks if all argument
names exist on both class and dataclass and logs mismatches.
Args:
cls: Any class type - but not an instance of a class. Pass type(x) where x is an instance
if class type is not easily available.
datacls: A corresponding dataclass for the above class.
ignore_args: (Optional) A list of string argument names which are forcibly ignored,
even if mismatched in the signature. Useful when a dataclass is a superset of the
arguments of a class.
remap_args: (Optional) A dictionary, mapping an argument name that exists (in either the
class or its dataclass), to another name. Useful when argument names are mismatched between
a class and its dataclass due to indirect instantiation via a helper method.
Returns:
A tuple containing information about the analysis:
1) A bool value which is True if the signatures matched exactly / after ignoring values.
False otherwise.
2) A set of arguments names that exist in the class, but *do not* exist in the dataclass.
If exact signature match occurs, this will be None instead.
3) A set of argument names that exist in the data class, but *do not* exist in the class itself.
If exact signature match occurs, this will be None instead.
"""
class_sig = inspect.signature(cls.__init__)
class_params = dict(**class_sig.parameters)
class_params.pop('self')
dataclass_sig = inspect.signature(datacls)
dataclass_params = dict(**dataclass_sig.parameters)
dataclass_params.pop("_target_", None)
class_params = set(class_params.keys())
dataclass_params = set(dataclass_params.keys())
if remap_args is not None:
for original_arg, new_arg in remap_args.items():
if original_arg in class_params:
class_params.remove(original_arg)
class_params.add(new_arg)
logging.info(f"Remapped {original_arg} -> {new_arg} in {cls.__name__}")
if original_arg in dataclass_params:
dataclass_params.remove(original_arg)
dataclass_params.add(new_arg)
logging.info(f"Remapped {original_arg} -> {new_arg} in {datacls.__name__}")
if ignore_args is not None:
ignore_args = set(ignore_args)
class_params = class_params - ignore_args
dataclass_params = dataclass_params - ignore_args
logging.info(f"Removing ignored arguments - {ignore_args}")
intersection = set.intersection(class_params, dataclass_params)
subset_cls = class_params - intersection
subset_datacls = dataclass_params - intersection
if (len(class_params) != len(dataclass_params)) or len(subset_cls) > 0 or len(subset_datacls) > 0:
logging.error(f"Class {cls.__name__} arguments do not match " f"Dataclass {datacls.__name__}!")
if len(subset_cls) > 0:
logging.error(f"Class {cls.__name__} has additional arguments :\n" f"{subset_cls}")
if len(subset_datacls):
logging.error(f"Dataclass {datacls.__name__} has additional arguments :\n{subset_datacls}")
return False, subset_cls, subset_datacls
else:
return True, None, None
+18
View File
@@ -0,0 +1,18 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.utils.decorators.deprecated import deprecated
from nemo.utils.decorators.experimental import experimental
from nemo.utils.decorators.port_docs import add_port_docs
+72
View File
@@ -0,0 +1,72 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = [
'deprecated',
]
import functools
import wrapt
from nemo.utils import logging
# Remember which deprecation warnings have been printed already.
_PRINTED_WARNING = {}
def deprecated(wrapped=None, version=None, explanation=None):
""" Decorator class used for indicating that a function is deprecated and going to be removed.
Tracks down which functions printed the warning and will print it only once per function.
"""
if wrapped is None:
return functools.partial(deprecated, version=version, explanation=explanation)
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
"""
Method prints the adequate warning (only once per function) when
required and calls the function func, passing the original arguments,
i.e. version and explanation.
Args:
version: Version in which the function will be removed (optional)
explanation: Additional explanation (optional), e.g. use method ``blabla instead``.
"""
# Check if we already warned about that function.
if wrapped.__name__ not in _PRINTED_WARNING.keys():
# Add to list so we won't print it again.
_PRINTED_WARNING[wrapped.__name__] = True
# Prepare the warning message.
msg = "Function ``{}`` is deprecated.".format(wrapped.__name__)
# Optionally, add version and alternative.
if version is not None:
msg = msg + " It is going to be removed in "
msg = msg + "the {} version.".format(version)
if explanation is not None:
msg = msg + " " + explanation
# Display the deprecated warning.
logging.warning(msg)
# Call the function.
return wrapped(*args, **kwargs)
return wrapper(wrapped)
+33
View File
@@ -0,0 +1,33 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = ['experimental']
from nemo.utils import logging
def experimental(cls):
""" Decorator which indicates that module is experimental.
Use it to mark experimental or research modules.
"""
def wrapped(cls):
logging.warning(
f'Module {cls} is experimental, not ready for production and is not fully supported. Use at your own risk.'
)
return cls
return wrapped(cls=cls)
+90
View File
@@ -0,0 +1,90 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The "add_port_docs" decorator is needed to nicely generate neural types in Sphynx for input and output ports
__all__ = [
'add_port_docs',
]
import functools
import sys
import wrapt
def _normalize_docstring(docstring):
"""Normalizes the docstring.
Replaces tabs with spaces, removes leading and trailing blanks lines, and
removes any indentation.
Copied from PEP-257:
https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
Args:
docstring: the docstring to normalize
Returns:
The normalized docstring
"""
if not docstring:
return ''
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
# (we use sys.maxsize because sys.maxint doesn't exist in Python 3)
indent = sys.maxsize
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxsize:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join(trimmed)
def add_port_docs(wrapped=None, instance=None, value=''):
if wrapped is None:
return functools.partial(add_port_docs, value=value)
@wrapt.decorator
def wrapper(wrapped, instance=None, args=None, kwargs=None):
return wrapped(*args, **kwargs)
decorated = wrapper(wrapped)
try:
port_2_ntype = decorated(instance)
except:
port_2_ntype = None
port_description = ""
if port_2_ntype is not None:
for port, ntype in port_2_ntype.items():
port_description += "* *" + port + "* : " + str(ntype)
port_description += "\n\n"
__doc__ = _normalize_docstring(wrapped.__doc__) + '\n\n' + str(port_description)
__doc__ = _normalize_docstring(__doc__)
wrapt.FunctionWrapper.__setattr__(decorated, "__doc__", __doc__)
return decorated
+207
View File
@@ -0,0 +1,207 @@
# The MIT Licence (MIT)
#
# Copyright (c) 2016 YunoJuno Ltd
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Vendored dependency from : https://github.com/yunojuno/python-env-utils/blob/master/env_utils/utils.py
#
#
# Modified by NVIDIA
#
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import decimal
import json
import os
from dateutil import parser
__all__ = [
"get_env",
"get_envbool",
"get_envint",
"get_envfloat",
"get_envdecimal",
"get_envdate",
"get_envdatetime",
"get_envlist",
"get_envdict",
"CoercionError",
"RequiredSettingMissingError",
]
class CoercionError(Exception):
"""Custom error raised when a value cannot be coerced."""
def __init__(self, key, value, func):
msg = "Unable to coerce '{}={}' using {}.".format(key, value, func.__name__)
super(CoercionError, self).__init__(msg)
class RequiredSettingMissingError(Exception):
"""Custom error raised when a required env var is missing."""
def __init__(self, key):
msg = "Required env var '{}' is missing.".format(key)
super(RequiredSettingMissingError, self).__init__(msg)
def _get_env(key, default=None, coerce=lambda x: x, required=False):
"""
Return env var coerced into a type other than string.
This function extends the standard os.getenv function to enable
the coercion of values into data types other than string (all env
vars are strings by default).
Args:
key: string, the name of the env var to look up
Kwargs:
default: the default value to return if the env var does not exist. NB the
default value is **not** coerced, and is assumed to be of the correct type.
coerce: a function that is used to coerce the value returned into
another type
required: bool, if True, then a RequiredSettingMissingError error is raised
if the env var does not exist.
Returns the env var, passed through the coerce function
"""
try:
value = os.environ[key]
except KeyError:
if required is True:
raise RequiredSettingMissingError(key)
else:
return default
try:
return coerce(value)
except Exception:
raise CoercionError(key, value, coerce)
# standard type coercion functions
def _bool(value):
if isinstance(value, bool):
return value
return not (value is None or value.lower() in ("false", "0", "no", "n", "f", "none"))
def _int(value):
return int(value)
def _float(value):
return float(value)
def _decimal(value):
return decimal.Decimal(value)
def _dict(value):
return json.loads(value)
def _datetime(value):
return parser.parse(value)
def _date(value):
return parser.parse(value).date()
def get_env(key, *default, **kwargs):
"""
Return env var.
This is the parent function of all other get_foo functions,
and is responsible for unpacking args/kwargs into the values
that _get_env expects (it is the root function that actually
interacts with environ).
Args:
key: string, the env var name to look up.
default: (optional) the value to use if the env var does not
exist. If this value is not supplied, then the env var is
considered to be required, and a RequiredSettingMissingError
error will be raised if it does not exist.
Kwargs:
coerce: a func that may be supplied to coerce the value into
something else. This is used by the default get_foo functions
to cast strings to builtin types, but could be a function that
returns a custom class.
Returns the env var, coerced if required, and a default if supplied.
"""
assert len(default) in (0, 1), "Too many args supplied."
func = kwargs.get('coerce', lambda x: x)
required = len(default) == 0
default = default[0] if not required else None
return _get_env(key, default=default, coerce=func, required=required)
def get_envbool(key, *default):
"""Return env var cast as boolean."""
return get_env(key, *default, coerce=_bool)
def get_envint(key, *default):
"""Return env var cast as integer."""
return get_env(key, *default, coerce=_int)
def get_envfloat(key, *default):
"""Return env var cast as float."""
return get_env(key, *default, coerce=_float)
def get_envdecimal(key, *default):
"""Return env var cast as Decimal."""
return get_env(key, *default, coerce=_decimal)
def get_envdate(key, *default):
"""Return env var as a date."""
return get_env(key, *default, coerce=_date)
def get_envdatetime(key, *default):
"""Return env var as a datetime."""
return get_env(key, *default, coerce=_datetime)
def get_envlist(key, *default, **kwargs):
"""Return env var as a list."""
separator = kwargs.get('separator', ' ')
return get_env(key, *default, coerce=lambda x: x.split(separator))
def get_envdict(key, *default):
"""Return env var as a dict."""
return get_env(key, *default, coerce=_dict)
+19
View File
@@ -0,0 +1,19 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class NeMoBaseException(Exception):
""" NeMo Base Exception. All exceptions created in NeMo should inherit from this class"""
pass
+715
View File
@@ -0,0 +1,715 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import sys
import time
from copy import deepcopy
from dataclasses import dataclass
from pathlib import Path
from shutil import copy, move
from typing import Any, Dict, List, Optional, Union
import torch
from hydra.core.hydra_config import HydraConfig
from hydra.utils import get_original_cwd
from omegaconf import DictConfig, OmegaConf, open_dict
from pytorch_lightning.callbacks import ModelCheckpoint
from pytorch_lightning.loggers import LoggerCollection as _LoggerCollection
from pytorch_lightning.loggers import TensorBoardLogger, WandbLogger
from pytorch_lightning.utilities import rank_zero_only
from nemo.constants import NEMO_ENV_VARNAME_VERSION
from nemo.utils import app_state, logging
from nemo.utils.app_state import AppState
from nemo.utils.exceptions import NeMoBaseException
from nemo.utils.get_rank import is_global_rank_zero
from nemo.utils.lightning_logger_patch import add_filehandlers_to_pl_logger
class NotFoundError(NeMoBaseException):
""" Raised when a file or folder is not found"""
class LoggerMisconfigurationError(NeMoBaseException):
""" Raised when a mismatch between trainer.logger and exp_manager occurs"""
def __init__(self, message):
message = (
message
+ " You can disable lighning's trainer from creating a logger by passing logger=False to its constructor."
)
super().__init__(message)
class CheckpointMisconfigurationError(NeMoBaseException):
""" Raised when a mismatch between trainer.callbacks and exp_manager occurs"""
@dataclass
class CallbackParams:
filepath: Optional[str] = None # Deprecated
dirpath: Optional[str] = None # If None, exp_manager will attempt to handle the filepath
filename: Optional[str] = None # If None, exp_manager will attempt to handle the filepath
monitor: Optional[str] = "val_loss"
verbose: Optional[bool] = True
save_last: Optional[bool] = True
save_top_k: Optional[int] = 3
save_weights_only: Optional[bool] = False
mode: Optional[str] = "min"
period: Optional[int] = 1
prefix: Optional[str] = None # If None, exp_manager will attempt to handle the filepath
postfix: str = ".nemo"
save_best_model: bool = False
always_save_nemo: bool = False
@dataclass
class ExpManagerConfig:
# Log dir creation parameters
explicit_log_dir: Optional[str] = None
exp_dir: Optional[str] = None
name: Optional[str] = None
version: Optional[str] = None
use_datetime_version: Optional[bool] = True
resume_if_exists: Optional[bool] = False
resume_past_end: Optional[bool] = False
resume_ignore_no_checkpoint: Optional[bool] = False
# Logging parameters
create_tensorboard_logger: Optional[bool] = True
summary_writer_kwargs: Optional[Dict[Any, Any]] = None
create_wandb_logger: Optional[bool] = False
wandb_logger_kwargs: Optional[Dict[Any, Any]] = None
# Checkpointing parameters
create_checkpoint_callback: Optional[bool] = True
checkpoint_callback_params: Optional[CallbackParams] = CallbackParams()
# Additional exp_manager arguments
files_to_copy: Optional[List[str]] = None
def exp_manager(trainer: 'pytorch_lightning.Trainer', cfg: Optional[Union[DictConfig, Dict]] = None) -> Path:
"""
exp_manager is a helper function used to manage folders for experiments. It follows the pytorch lightning paradigm
of exp_dir/model_or_experiment_name/version. If the lightning trainer has a logger, exp_manager will get exp_dir,
name, and version from the logger. Otherwise it will use the exp_dir and name arguments to create the logging
directory. exp_manager also allows for explicit folder creation via explicit_log_dir.
The version can be a datetime string or an integer. Datestime version can be disabled if use_datetime_version is set
to False. It optionally creates TensorBoardLogger, WandBLogger, ModelCheckpoint objects from pytorch lightning.
It copies sys.argv, and git information if available to the logging directory. It creates a log file for each
process to log their output into.
exp_manager additionally has a resume feature (resume_if_exists) which can be used to continuing training from
the constructed log_dir. When you need to continue the training repeatedly (like on a cluster which you need
multiple consecutive jobs), you need to avoid creating the version folders. Therefore from v1.0.0, when
resume_if_exists is set to True, creating the version folders is ignored.
Args:
trainer (pytorch_lightning.Trainer): The lightning trainer.
cfg (DictConfig, dict): Can have the following keys:
- explicit_log_dir (str, Path): Can be used to override exp_dir/name/version folder creation. Defaults to
None, which will use exp_dir, name, and version to construct the logging directory.
- exp_dir (str, Path): The base directory to create the logging directory. Defaults to None, which logs to
./nemo_experiments.
- name (str): The name of the experiment. Defaults to None which turns into "default" via name = name or
"default".
- version (str): The version of the experiment. Defaults to None which uses either a datetime string or
lightning's TensorboardLogger system of using version_{int}.
- use_datetime_version (bool): Whether to use a datetime string for version. Defaults to True.
- resume_if_exists (bool): Whether this experiment is resuming from a previous run. If True, it sets
trainer.resume_from_checkpoint so that the trainer should auto-resume. exp_manager will move files
under log_dir to log_dir/run_{int}. Defaults to False. From v1.0.0, when resume_if_exists is True,
we would not create version folders to make it easier to find the log folder for next runs.
- resume_past_end (bool): exp_manager errors out if resume_if_exists is True and a checkpoint matching
*end.ckpt indicating a previous training run fully completed. This behaviour can be disabled, in which
case the *end.ckpt will be loaded by setting resume_past_end to True. Defaults to False.
- resume_ignore_no_checkpoint (bool): exp_manager errors out if resume_if_exists is True and no checkpoint
could be found. This behaviour can be disabled, in which case exp_manager will print a message and
continue without restoring, by setting resume_ignore_no_checkpoint to True. Defaults to False.
- create_tensorboard_logger (bool): Whether to create a tensorboard logger and attach it to the pytorch
lightning trainer. Defaults to True.
- summary_writer_kwargs (dict): A dictionary of kwargs that can be passed to lightning's TensorboardLogger
class. Note that log_dir is passed by exp_manager and cannot exist in this dict. Defaults to None.
- create_wandb_logger (bool): Whether to create a Weights and Baises logger and attach it to the pytorch
lightning trainer. Defaults to False.
- wandb_logger_kwargs (dict): A dictionary of kwargs that can be passed to lightning's WandBLogger
class. Note that name and project are required parameters if create_wandb_logger is True.
Defaults to None.
- create_checkpoint_callback (bool): Whether to create a ModelCheckpoint callback and attach it to the
pytorch lightning trainer. The ModelCheckpoint saves the top 3 models with the best "val_loss", the most
recent checkpoint under *last.ckpt, and the final checkpoint after training completes under *end.ckpt.
Defaults to True.
- files_to_copy (list): A list of files to copy to the experiment logging directory. Defaults to None which
copies no files.
returns:
log_dir (Path): The final logging directory where logging files are saved. Usually the concatenation of
exp_dir, name, and version.
"""
# Add rank information to logger
# Note: trainer.global_rank and trainer.is_global_zero are not set until trainer.fit, so have to hack around it
global_rank = trainer.node_rank * trainer.num_gpus + int(os.environ.get("LOCAL_RANK", 0))
logging.rank = global_rank
if cfg is None:
logging.error("exp_manager did not receive a cfg argument. It will be disabled.")
return
if trainer.fast_dev_run:
logging.info("Trainer was called with fast_dev_run. exp_manager will return without any functionality.")
return
# Ensure passed cfg is compliant with ExpManagerConfig
schema = OmegaConf.structured(ExpManagerConfig)
if isinstance(cfg, dict):
cfg = OmegaConf.create(cfg)
elif not isinstance(cfg, DictConfig):
raise ValueError(f"cfg was type: {type(cfg)}. Expected either a dict or a DictConfig")
cfg = OmegaConf.create(OmegaConf.to_container(cfg, resolve=True))
cfg = OmegaConf.merge(schema, cfg)
error_checks(trainer, cfg) # Ensures that trainer options are compliant with NeMo and exp_manager arguments
log_dir, exp_dir, name, version = get_log_dir(
trainer=trainer,
exp_dir=cfg.exp_dir,
name=cfg.name,
version=cfg.version,
explicit_log_dir=cfg.explicit_log_dir,
use_datetime_version=cfg.use_datetime_version,
resume_if_exists=cfg.resume_if_exists,
)
if cfg.resume_if_exists:
check_resume(trainer, log_dir, cfg.resume_past_end, cfg.resume_ignore_no_checkpoint)
checkpoint_name = name
# If name returned from get_log_dir is "", use cfg.name for checkpointing
if checkpoint_name is None or checkpoint_name == '':
checkpoint_name = cfg.name or "default"
cfg.name = name # Used for configure_loggers so that the log_dir is properly set even if name is ""
cfg.version = version
# update app_state with log_dir, exp_dir, etc
app_state = AppState()
app_state.log_dir = log_dir
app_state.exp_dir = exp_dir
app_state.name = name
app_state.version = version
app_state.checkpoint_name = checkpoint_name
app_state.create_checkpoint_callback = cfg.create_checkpoint_callback
app_state.checkpoint_callback_params = cfg.checkpoint_callback_params
# Create the logging directory if it does not exist
os.makedirs(log_dir, exist_ok=True) # Cannot limit creation to global zero as all ranks write to own log file
logging.info(f'Experiments will be logged at {log_dir}')
trainer._default_root_dir = log_dir
# Handle Loggers by creating file and handle DEBUG statements
log_file = log_dir / f'nemo_log_globalrank-{global_rank}_localrank-{int(os.environ.get("LOCAL_RANK", 0))}.txt'
logging.add_file_handler(log_file)
# For some reason, LearningRateLogger requires trainer to have a logger. Safer to create logger on all ranks
# not just global rank 0.
if cfg.create_tensorboard_logger or cfg.create_wandb_logger:
configure_loggers(
trainer,
exp_dir,
cfg.name,
cfg.version,
cfg.create_tensorboard_logger,
cfg.summary_writer_kwargs,
cfg.create_wandb_logger,
cfg.wandb_logger_kwargs,
)
if cfg.create_checkpoint_callback:
configure_checkpointing(trainer, log_dir, checkpoint_name, cfg.checkpoint_callback_params)
if is_global_rank_zero():
# Move files_to_copy to folder and add git information if present
if cfg.files_to_copy:
for _file in cfg.files_to_copy:
copy(Path(_file), log_dir)
# Create files for cmd args and git info
with open(log_dir / 'cmd-args.log', 'w') as _file:
_file.write(" ".join(sys.argv))
# Try to get git hash
git_repo, git_hash = get_git_hash()
if git_repo:
with open(log_dir / 'git-info.log', 'w') as _file:
_file.write(f'commit hash: {git_hash}')
_file.write(get_git_diff())
# Add err_file logging to global_rank zero
logging.add_err_file_handler(log_dir / 'nemo_error_log.txt')
# Add lightning file logging to global_rank zero
add_filehandlers_to_pl_logger(log_dir / 'lightning_logs.txt', log_dir / 'nemo_error_log.txt')
return log_dir
def error_checks(trainer: 'pytorch_lightning.Trainer', cfg: Optional[Union[DictConfig, Dict]] = None):
"""
Checks that the passed trainer is compliant with NeMo and exp_manager's passed configuration. Checks that:
- Throws error when hydra has changed the working directory. This causes issues with lightning's DDP
- Throws error when trainer has loggers defined but create_tensorboard_logger or create_WandB_logger is True
- Prints error messages when 1) run on multi-node and not Slurm, and 2) run on multi-gpu without DDP
"""
if HydraConfig.initialized() and get_original_cwd() != os.getcwd():
raise ValueError(
"Hydra changed the working directory. This interferes with ExpManger's functionality. Please pass "
"hydra.run.dir=. to your python script."
)
if trainer.logger is not None and (cfg.create_tensorboard_logger or cfg.create_wandb_logger):
raise LoggerMisconfigurationError(
"The pytorch lightning trainer that was passed to exp_manager contained a logger, and either "
f"create_tensorboard_logger: {cfg.create_tensorboard_logger} or create_wandb_logger: "
f"{cfg.create_wandb_logger} was set to True. These can only be used if trainer does not already have a"
" logger."
)
if trainer.num_nodes > 1 and not check_slurm(trainer):
logging.error(
"You are running multi-node training without SLURM handling the processes."
" Please note that this is not tested in NeMo and could result in errors."
)
if trainer.num_gpus > 1 and not trainer.use_ddp:
logging.error(
"You are running multi-gpu without ddp.Please note that this is not tested in NeMo and could result in "
"errors."
)
def check_resume(
trainer: 'pytorch_lightning.Trainer',
log_dir: str,
resume_past_end: bool = False,
resume_ignore_no_checkpoint: bool = False,
):
"""Checks that resume=True was used correctly with the arguments pass to exp_manager. Sets
trainer.resume_from_checkpoint as necessary.
Returns:
log_dir (Path): the log_dir
exp_dir (str): the base exp_dir without name nor version
name (str): The name of the experiment
version (str): The version of the experiment
Raises:
NotFoundError: If resume is True, resume_ignore_no_checkpoint is False, and checkpoints could not be found.
ValueError: If resume is True, and there were more than 1 checkpoint could found.
"""
if not log_dir:
raise ValueError(f"Resuming requires the log_dir {log_dir} to be passed to exp_manager")
checkpoint_dir = Path(Path(log_dir) / "checkpoints")
checkpoint = None
end_checkpoints = list(checkpoint_dir.rglob("*end.ckpt"))
last_checkpoints = list(checkpoint_dir.rglob("*last.ckpt"))
if not checkpoint_dir.exists():
if resume_ignore_no_checkpoint:
logging.warning(
f"There was no checkpoint folder at checkpoint_dir :{checkpoint_dir}. Training from scratch."
)
return
else:
raise NotFoundError(f"There was no checkpoint folder at checkpoint_dir :{checkpoint_dir}. Cannot resume.")
elif len(end_checkpoints) > 0:
if resume_past_end:
if len(end_checkpoints) > 1:
if 'mp_rank' in str(end_checkpoints[0]):
checkpoint = end_checkpoints[0]
else:
raise ValueError(f"Multiple checkpoints {end_checkpoints} that matches *end.ckpt.")
logging.info(f"Resuming from {end_checkpoints[0]}")
else:
raise ValueError(
f"Found {end_checkpoints[0]} indicating that the last training run has already completed."
)
elif not len(last_checkpoints) > 0:
if resume_ignore_no_checkpoint:
logging.warning(f"There were no checkpoints found in {checkpoint_dir}. Training from scratch.")
return
else:
raise NotFoundError(f"There were no checkpoints found in {checkpoint_dir}. Cannot resume.")
elif len(last_checkpoints) > 1:
if 'mp_rank' in str(last_checkpoints[0]):
checkpoint = last_checkpoints[0]
else:
raise ValueError(f"Multiple checkpoints {last_checkpoints} that matches *last.ckpt.")
else:
logging.info(f"Resuming from {last_checkpoints[0]}")
checkpoint = last_checkpoints[0]
trainer.resume_from_checkpoint = str(checkpoint)
if is_global_rank_zero():
# Check to see if any files exist that need to be moved
files_to_move = []
for child in Path(log_dir).iterdir():
if child.is_file():
files_to_move.append(child)
if len(files_to_move) > 0:
# Move old files to a new folder
other_run_dirs = Path(log_dir).glob("run_*")
run_count = 0
for fold in other_run_dirs:
if fold.is_dir():
run_count += 1
new_run_dir = Path(Path(log_dir) / f"run_{run_count}")
new_run_dir.mkdir()
for _file in files_to_move:
move(str(_file), str(new_run_dir))
def check_explicit_log_dir(
trainer: 'pytorch_lightning.Trainer', explicit_log_dir: [Path, str], exp_dir: str, name: str, version: str
) -> (Path, str, str, str):
""" Checks that the passed arguments are compatible with explicit_log_dir.
Returns:
log_dir (Path): the log_dir
exp_dir (str): the base exp_dir without name nor version
name (str): The name of the experiment
version (str): The version of the experiment
Raise:
LoggerMisconfigurationError
"""
if trainer.logger is not None:
raise LoggerMisconfigurationError(
"The pytorch lightning trainer that was passed to exp_manager contained a logger and explicit_log_dir: "
f"{explicit_log_dir} was pass to exp_manager. Please remove the logger from the lightning trainer."
)
# Checking only (explicit_log_dir) vs (exp_dir and version).
# The `name` will be used as the actual name of checkpoint/archive.
if exp_dir or version:
logging.error(
f"exp_manager received explicit_log_dir: {explicit_log_dir} and at least one of exp_dir: {exp_dir}, "
f"or version: {version}. Please note that exp_dir, name, and version will be ignored."
)
if is_global_rank_zero() and Path(explicit_log_dir).exists():
logging.warning(f"Exp_manager is logging to {explicit_log_dir}, but it already exists.")
return Path(explicit_log_dir), str(explicit_log_dir), "", ""
def get_log_dir(
trainer: 'pytorch_lightning.Trainer',
exp_dir: str = None,
name: str = None,
version: str = None,
explicit_log_dir: str = None,
use_datetime_version: bool = True,
resume_if_exists: bool = False,
) -> (Path, str, str, str):
"""
Obtains the log_dir used for exp_manager.
Returns:
log_dir (Path): the log_dir
exp_dir (str): the base exp_dir without name nor version
name (str): The name of the experiment
version (str): The version of the experiment
explicit_log_dir (str): The explicit path to the log folder. Defaults to False.
use_datetime_version (bool): Uses date and time as the version of the log folder. Defaults to True.
resume_if_exists (bool): if resume_if_exists of the exp_manager's config is enabled or not. When enabled, the
version folders would not get created.
Raise:
LoggerMisconfigurationError: If trainer is incompatible with arguments
NotFoundError: If resume is True, resume_ignore_no_checkpoint is False, and checkpoints could not be found.
ValueError: If resume is True, and there were more than 1 checkpoint could found.
"""
if explicit_log_dir: # If explicit log_dir was passed, short circuit
return check_explicit_log_dir(trainer, explicit_log_dir, exp_dir, name, version)
# Default exp_dir to ./nemo_experiments if None was passed
_exp_dir = exp_dir
if exp_dir is None:
_exp_dir = str(Path.cwd() / 'nemo_experiments')
# If the user has already defined a logger for the trainer, use the logger defaults for logging directory
if trainer.logger is not None:
if trainer.logger.save_dir:
if exp_dir:
raise LoggerMisconfigurationError(
"The pytorch lightning trainer that was passed to exp_manager contained a logger, the logger's "
f"save_dir was not None, and exp_dir ({exp_dir}) was not None. If trainer.logger.save_dir "
"exists, exp_manager will use trainer.logger.save_dir as the logging directory and exp_dir "
"must be None."
)
_exp_dir = trainer.logger.save_dir
if name:
raise LoggerMisconfigurationError(
"The pytorch lightning trainer that was passed to exp_manager contained a logger, and name: "
f"{name} was also passed to exp_manager. If the trainer contains a "
"logger, exp_manager will use trainer.logger.name, and name passed to exp_manager must be None."
)
name = trainer.logger.name
version = f"version_{trainer.logger.version}"
# Use user-defined exp_dir, project_name, exp_name, and versioning options
else:
name = name or "default"
version = version or os.environ.get(NEMO_ENV_VARNAME_VERSION, None)
if not version:
if resume_if_exists:
logging.warning(
"No version folders would be created under the log folder as 'resume_if_exists' is enabled."
)
version = None
elif is_global_rank_zero():
if use_datetime_version:
version = time.strftime('%Y-%m-%d_%H-%M-%S')
else:
tensorboard_logger = TensorBoardLogger(save_dir=Path(_exp_dir), name=name, version=version)
version = f"version_{tensorboard_logger.version}"
os.environ[NEMO_ENV_VARNAME_VERSION] = "" if version is None else version
log_dir = Path(_exp_dir) / Path(str(name)) / Path("" if version is None else str(version))
return log_dir, str(_exp_dir), name, version
def get_git_hash():
"""
Helper function that tries to get the commit hash if running inside a git folder
returns:
Bool: Whether the git subprocess ran without error
str: git subprocess output or error message
"""
try:
return (
True,
subprocess.check_output(['git', 'rev-parse', 'HEAD'], stderr=subprocess.STDOUT).decode(),
)
except subprocess.CalledProcessError as err:
return False, "{}\n".format(err.output.decode("utf-8"))
def get_git_diff():
"""
Helper function that tries to get the git diff if running inside a git folder
returns:
Bool: Whether the git subprocess ran without error
str: git subprocess output or error message
"""
try:
return subprocess.check_output(['git', 'diff'], stderr=subprocess.STDOUT).decode()
except subprocess.CalledProcessError as err:
return "{}\n".format(err.output.decode("utf-8"))
class LoggerList(_LoggerCollection):
""" A thin wrapper on Lightning's LoggerCollection such that name and version are better aligned with exp_manager
"""
def __init__(self, _logger_iterable, nemo_name=None, nemo_version=""):
super().__init__(_logger_iterable)
self._nemo_name = nemo_name
self._nemo_version = nemo_version
@property
def name(self) -> str:
return self._nemo_name
@property
def version(self) -> str:
return self._nemo_version
def configure_loggers(
trainer: 'pytorch_lightning.Trainer',
exp_dir: [Path, str],
name: str,
version: str,
create_tensorboard_logger: bool,
summary_writer_kwargs: dict,
create_wandb_logger: bool,
wandb_kwargs: dict,
):
""" Creates TensorboardLogger and/or WandBLogger and attach them to trainer. Raises ValueError if
summary_writer_kwargs or wandb_kwargs are misconfigured.
"""
# Potentially create tensorboard logger and/or WandBLogger
logger_list = []
if create_tensorboard_logger:
if summary_writer_kwargs is None:
summary_writer_kwargs = {}
elif "log_dir" in summary_writer_kwargs:
raise ValueError(
"You cannot pass `log_dir` as part of `summary_writer_kwargs`. `log_dir` is handled by lightning's "
"TensorBoardLogger logger."
)
tensorboard_logger = TensorBoardLogger(save_dir=exp_dir, name=name, version=version, **summary_writer_kwargs)
logger_list.append(tensorboard_logger)
logging.info("TensorboardLogger has been set up")
if create_wandb_logger:
if wandb_kwargs is None:
wandb_kwargs = {}
if "name" not in wandb_kwargs and "project" not in wandb_kwargs:
raise ValueError("name and project are required for wandb_logger")
wandb_logger = WandbLogger(save_dir=exp_dir, version=version, **wandb_kwargs)
logger_list.append(wandb_logger)
logging.info("WandBLogger has been set up")
logger_list = (
LoggerList(logger_list, nemo_name=name, nemo_version=version) if len(logger_list) > 1 else logger_list[0]
)
trainer.logger_connector.configure_logger(logger_list)
class NeMoModelCheckpoint(ModelCheckpoint):
""" Light wrapper around Lightning's ModelCheckpoint to force a saved checkpoint on train_end
"""
def __init__(self, always_save_nemo=False, save_best_model=False, postfix=".nemo", **kwargs):
# Parse and store "extended" parameters: save_best model and postfix.
self.always_save_nemo = always_save_nemo
self.save_best_model = save_best_model
self.postfix = postfix
self.previous_best_path = ""
# `prefix` is deprecated
if 'prefix' in kwargs:
self.prefix = kwargs.pop('prefix')
else:
self.prefix = ""
# Call the parent class constructor with the remaining kwargs.
super().__init__(**kwargs)
@rank_zero_only
def on_save_checkpoint(self, trainer, pl_module, checkpoint):
output = super().on_save_checkpoint(trainer, pl_module, checkpoint)
if not self.always_save_nemo:
return output
# Load the best model and then re-save it
app_state = AppState()
# since we are creating tarfile artifacts we need to update .nemo path
app_state.model_restore_path = os.path.abspath(
os.path.expanduser(os.path.join(self.dirpath, self.prefix + self.postfix))
)
if self.save_best_model:
if not os.path.exists(self.best_model_path):
return output
if self.best_model_path == self.previous_best_path:
return output
self.previous_model_path = self.best_model_path
old_state_dict = deepcopy(pl_module.state_dict())
checkpoint = torch.load(self.best_model_path, map_location='cpu')
if 'state_dict' in checkpoint:
checkpoint = checkpoint['state_dict']
# get a new instanace of the model
pl_module.load_state_dict(checkpoint, strict=True)
pl_module.save_to(save_path=app_state.model_restore_path)
pl_module.load_state_dict(old_state_dict, strict=True)
else:
pl_module.save_to(save_path=app_state.model_restore_path)
return output
@rank_zero_only
def on_train_end(self, trainer, pl_module):
if trainer.fast_dev_run:
return None
app_state = AppState()
if app_state.model_parallel_size is not None:
return None
# TODO: make this work for model parallel, need to call on data parallel rank 0 and update best_model_path
# Load the best model and then re-save it
if self.save_best_model:
trainer.checkpoint_connector.restore(self.best_model_path, on_gpu=trainer.on_gpu)
pl_module.save_to(save_path=os.path.join(self.dirpath, self.prefix + self.postfix))
def configure_checkpointing(trainer: 'pytorch_lightning.Trainer', log_dir: Path, name: str, params: 'DictConfig'):
""" Adds ModelCheckpoint to trainer. Raises CheckpointMisconfigurationError if trainer already has a ModelCheckpoint
callback or if trainer.weights_save_path was passed to Trainer.
"""
for callback in trainer.callbacks:
if isinstance(callback, ModelCheckpoint):
raise CheckpointMisconfigurationError(
"The pytorch lightning trainer that was passed to exp_manager contained a ModelCheckpoint "
"and create_checkpoint_callback was set to True. Please either set create_checkpoint_callback "
"to False, or remove ModelCheckpoint from the lightning trainer"
)
if Path(trainer.weights_save_path) != Path.cwd():
raise CheckpointMisconfigurationError(
"The pytorch lightning was passed weights_save_path. This variable is ignored by exp_manager"
)
# Create the callback and attach it to trainer
if "filepath" in params:
if params.filepath is not None:
logging.warning("filepath is deprecated. Please switch to dirpath and filename instead")
if params.dirpath is None:
params.dirpath = Path(params.filepath).parent
if params.filename is None:
params.filename = Path(params.filepath).name
with open_dict(params):
del params["filepath"]
if params.dirpath is None:
params.dirpath = Path(log_dir / 'checkpoints')
if params.filename is None:
params.filename = f'{name}--{{{params.monitor}:.2f}}-{{epoch}}'
if params.prefix is None:
params.prefix = name
NeMoModelCheckpoint.CHECKPOINT_NAME_LAST = params.filename + '-last'
logging.debug(params.dirpath)
logging.debug(params.filename)
logging.debug(params.prefix)
if "val" in params.monitor:
if (
trainer.max_epochs is not None
and trainer.max_epochs != -1
and trainer.max_epochs < trainer.check_val_every_n_epoch
):
logging.error(
"The checkpoint callback was told to monitor a validation value but trainer.max_epochs("
f"{trainer.max_epochs}) was less than trainer.check_val_every_n_epoch({trainer.check_val_every_n_epoch}"
f"). It is very likely this run will fail with ModelCheckpoint(monitor='{params.monitor}') not found "
"in the returned metrics. Please ensure that validation is run within trainer.max_epochs."
)
elif trainer.max_steps is not None:
logging.warning(
"The checkpoint callback was told to monitor a validation value and trainer's max_steps was set to "
f"{trainer.max_steps}. Please ensure that max_steps will run for at least "
f"{trainer.check_val_every_n_epoch} epochs to ensure that checkpointing will not error out."
)
checkpoint_callback = NeMoModelCheckpoint(**params)
checkpoint_callback.last_model_path = trainer.resume_from_checkpoint or ""
trainer.callbacks.append(checkpoint_callback)
def check_slurm(trainer):
try:
return trainer.accelerator_connector.is_slurm_managing_tasks
except AttributeError:
return False
+225
View File
@@ -0,0 +1,225 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Callable, Dict, Optional, Type
import onnx
import torch
import torch.nn as nn
from nemo.utils import logging
apex_available = True
try:
from apex.normalization.fused_layer_norm import FusedLayerNorm
def replace_FusedLayerNorm(n: nn.Module) -> Optional[nn.BatchNorm2d]:
"""
Replaces Apex's FusedLayerNorm with nn.LayerNorm. This is required for ONNX export.
Args:
n: the FusedLayerNorm pytorch module to replace
Returns:
Equivalent LayerNorm module
"""
if not apex_available or not isinstance(n, FusedLayerNorm):
return None
dev = next(n.parameters()).device
mod = nn.LayerNorm(n.normalized_shape, eps=n.eps, elementwise_affine=n.elementwise_affine,).to(dev)
n_state = n.state_dict()
mod.load_state_dict(n_state)
return mod
default_Apex_replacements = {"FusedLayerNorm": replace_FusedLayerNorm}
except Exception as e:
default_Apex_replacements = {}
apex_available = False
def expand_Conv1D(conv1d: nn.Module) -> Optional[nn.Conv2d]:
"""
Expands a Conv1D into a Conv2D. This is required for many (closed source) commercial tools with poor support for 1D Convolutions in Onnx.
Args:
conv1d: the Conv1D pytorch module to expand
Returns:
conv2d: Conv2D module with identical weights and params
"""
if not isinstance(conv1d, nn.Conv1d):
return None
conv2d = nn.Conv2d(
conv1d.in_channels,
conv1d.out_channels,
kernel_size=(conv1d.kernel_size[0], 1),
stride=(conv1d.stride[0], 1),
padding=(conv1d.padding[0], 0),
dilation=(conv1d.dilation[0], 1),
groups=conv1d.groups,
padding_mode=conv1d.padding_mode,
).to(device=conv1d.weight.device, dtype=conv1d.weight.dtype)
conv2d.bias = conv1d.bias
conv2d.weight = nn.Parameter(conv1d.weight.unsqueeze(-1))
# check that expansion is valid
for _ in range(2):
sample_input = torch.rand(1, conv1d.in_channels, 256).to(
device=conv1d.weight.device, dtype=conv1d.weight.dtype
)
close = conv1d(sample_input).mean() - conv2d(sample_input.unsqueeze(-1)).squeeze().mean()
if close.abs() > 1.0:
raise ValueError("Unable to expand Conv1D to Conv2D")
return conv2d
def expand_BatchNorm1d(bn1d: nn.Module) -> Optional[nn.BatchNorm2d]:
"""
Expands a BatchNorm1d into a BatchNorm2d. This is required for many (closed source) commercial tools with poor support for BatchNorm1d in Onnx.
Args:
bn1d: the BatchNorm1d pytorch module to expand
Returns:
bn2d: BatchNorm2d module with identical weights and params
"""
if not isinstance(bn1d, nn.BatchNorm1d):
return None
mod = torch.nn.BatchNorm2d(
bn1d.num_features,
eps=bn1d.eps,
momentum=bn1d.momentum,
affine=bn1d.affine,
track_running_stats=bn1d.track_running_stats,
).to(device=conv1d.weight.device, dtype=conv1d.weight.dtype)
bn_state = bn1d.state_dict()
mod.load_state_dict(bn_state)
return mod
def expand_ConvTranspose1D(conv1d: nn.Module) -> Optional[nn.ConvTranspose2d]:
"""
Expands a Conv1D into a Conv2D. This is required for many (closed source) commercial tools with poor support for 1D Convolutions in Onnx.
Args:
conv1d: the Conv1D pytorch module to expand
Returns:
conv2d: Conv2D module with identical weights and params
"""
if not isinstance(conv1d, nn.ConvTranspose1d):
return None
conv2d = nn.ConvTranspose2d(
conv1d.in_channels,
conv1d.out_channels,
kernel_size=(conv1d.kernel_size[0], 1),
stride=(conv1d.stride[0], 1),
padding=(conv1d.padding[0], 0),
dilation=(conv1d.dilation[0], 1),
groups=conv1d.groups,
padding_mode=conv1d.padding_mode,
).to(device=conv1d.weight.device, dtype=conv1d.weight.dtype)
conv2d.bias = conv1d.bias
conv2d.weight = nn.Parameter(conv1d.weight.unsqueeze(-1))
# check that expansion is valid
for _ in range(2):
sample_input = torch.rand(1, conv1d.in_channels, 256).to(
device=conv1d.weight.device, dtype=conv1d.weight.dtype
)
close = conv1d(sample_input).mean() - conv2d(sample_input.unsqueeze(-1)).squeeze().mean()
if close.abs() > 1.0:
raise ValueError("Unable to expand Conv1D to Conv2D")
return conv2d
def simple_replace(BaseT: Type[nn.Module], DestT: Type[nn.Module]) -> Callable[[nn.Module], Optional[nn.Module]]:
"""
Generic function generator to replace BaseT module with DestT. BaseT and DestT should have same atrributes. No weights are copied.
Args:
BaseT : module type to replace
DestT : destination module type
Returns:
swap function to replace BaseT module with DestT
"""
def expansion_fn(mod: nn.Module) -> Optional[nn.Module]:
if not isinstance(mod, BaseT):
return None
args = [getattr(mod, name, None) for name in mod.__constants__]
out = DestT(*args)
return out
return expansion_fn
def swap_modules(model: nn.Module, mapping: Dict[str, nn.Module]):
"""
This function swaps nested modules as specified by "dot paths" in mod with a desired replacement. This allows
for swapping nested modules through arbitrary levels if children
NOTE: This occurs in place, if you want to preserve model then make sure to copy it first.
"""
for path, new_mod in mapping.items():
expanded_path = path.split(".")
parent_mod = model
for sub_path in expanded_path[:-1]:
parent_mod = parent_mod._modules[sub_path] # noqa
parent_mod._modules[expanded_path[-1]] = new_mod # noqa
return model
def replace_modules(
model: nn.Module, expansions: Dict[str, Callable[[nn.Module], Optional[nn.Module]]] = None
) -> nn.Module:
"""
Top-level function to replace modules in model, specified by class name with a desired replacement.
NOTE: This occurs in place, if you want to preserve model then make sure to copy it first.
Args:
model : top level module
expansions : replacement dictionary: module class name -> replacement function generator
Returns:
model, possibly modified in-place
"""
mapping: Dict[str, nn.Module] = {}
for name, m in model.named_modules():
m_type = type(m).__name__
if m_type in expansions:
swapped = expansions[m_type](m)
if swapped:
mapping[name] = swapped
logging.warning(f"Swapped {len(mapping)} modules")
swap_modules(model, mapping)
return model
default_1D_2D_replacements = {
"Conv1d": expand_Conv1D,
"ConvTranspose1d": expand_ConvTranspose1D,
"BatchNorm1d": expand_BatchNorm1d,
"AdaptiveAvgPool1d": simple_replace(nn.AdaptiveAvgPool1d, nn.AdaptiveAvgPool2d),
"AvgPool1d": simple_replace(nn.AvgPool1d, nn.AvgPool2d),
}
def replace_for_export(model: nn.Module, replace_1D_2D: bool = False) -> nn.Module:
"""
Top-level function to replace default set of modules in model
NOTE: This occurs in place, if you want to preserve model then make sure to copy it first.
Args:
model : top level module
replace_1D_2D : include 1D -> 2D replacements
Returns:
model, possibly modified in-place
"""
replace_modules(model, default_Apex_replacements)
if replace_1D_2D:
# TODO: add squeeze/unsqueeze
replace_modules(model, default_1D_2D_replacements)
+13
View File
@@ -0,0 +1,13 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+135
View File
@@ -0,0 +1,135 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from nemo.utils.formatters.colors import Fore as ForegroundColors
from nemo.utils.formatters.utils import check_color_support, to_unicode
__all__ = ["BaseNeMoFormatter"]
class BaseFormatter(logging.Formatter):
"""
Log formatter used in Tornado. Key features of this formatter are:
* Color support when logging to a terminal that supports it.
* Timestamps on every log line.
* Robust against str/bytes encoding problems.
"""
DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_COLORS = {
logging.DEBUG: ForegroundColors.CYAN,
logging.INFO: ForegroundColors.GREEN,
logging.WARNING: ForegroundColors.YELLOW,
logging.ERROR: ForegroundColors.MAGENTA,
logging.CRITICAL: ForegroundColors.RED,
}
def __init__(self, color=True, fmt=None, datefmt=None, colors=None):
r"""
:arg bool color: Enables color support.
:arg string fmt: Log message format.
It will be applied to the attributes dict of log records. The
text between ``%(color)s`` and ``%(end_color)s`` will be colored
depending on the level if color support is on.
:arg dict colors: color mappings from logging level to terminal color
code
:arg string datefmt: Datetime format.
Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``.
.. versionchanged:: 3.2
Added ``fmt`` and ``datefmt`` arguments.
"""
if fmt is None:
fmt = self.DEFAULT_FORMAT
if datefmt is None:
datefmt = self.DEFAULT_DATE_FORMAT
if colors is None:
colors = self.DEFAULT_COLORS
logging.Formatter.__init__(self, datefmt=datefmt)
self._fmt = fmt
self._colors = {}
self._normal = ""
if color and check_color_support():
self._colors = colors
self._normal = ForegroundColors.RESET
def format(self, record):
try:
message = record.getMessage()
assert isinstance(message, str) # guaranteed by logging
# Encoding notes: The logging module prefers to work with character
# strings, but only enforces that log messages are instances of
# basestring. In python 2, non-ascii bytestrings will make
# their way through the logging framework until they blow up with
# an unhelpful decoding error (with this formatter it happens
# when we attach the prefix, but there are other opportunities for
# exceptions further along in the framework).
#
# If a byte string makes it this far, convert it to unicode to
# ensure it will make it out to the logs. Use repr() as a fallback
# to ensure that all byte strings can be converted successfully,
# but don't do it by default so we don't add extra quotes to ascii
# bytestrings. This is a bit of a hacky place to do this, but
# it's worth it since the encoding errors that would otherwise
# result are so useless (and tornado is fond of using utf8-encoded
# byte strings wherever possible).
record.message = to_unicode(message)
except Exception as e:
record.message = "Bad message (%r): %r" % (e, record.__dict__)
record.asctime = self.formatTime(record, self.datefmt)
if record.levelno in self._colors:
record.color = self._colors[record.levelno]
record.end_color = self._normal
else:
record.color = record.end_color = ""
formatted = self._fmt % record.__dict__
if record.exc_info:
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)
if record.exc_text:
# exc_text contains multiple lines. We need to _safe_unicode
# each line separately so that non-utf8 bytes don't cause
# all the newlines to turn into '\n'.
lines = [formatted.rstrip()]
lines.extend(to_unicode(ln) for ln in record.exc_text.split("\n"))
formatted = "\n".join(lines)
return formatted.replace("\n", "\n ")
class BaseNeMoFormatter(BaseFormatter):
DEFAULT_FORMAT = "%(color)s[NeMo %(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s"
class DebugNeMoFormatter(BaseFormatter):
DEFAULT_FORMAT = (
"%(color)s[NeMo %(levelname)1.1s %(asctime)s %(module)s:%(lineno)d rank:%(rank)s]%(end_color)s %(message)s"
)
+121
View File
@@ -0,0 +1,121 @@
# Source: https://github.com/tartley/colorama/blob/master/colorama/ansi.py
# Copyright: Jonathan Hartley 2013. BSD 3-Clause license.
#
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
CSI = "\033["
OSC = "\033]"
BEL = "\007"
def code_to_chars(code):
return CSI + str(code) + "m"
def set_title(title):
return OSC + "2;" + title + BEL
def clear_screen(mode=2):
return CSI + str(mode) + "J"
def clear_line(mode=2):
return CSI + str(mode) + "K"
class AnsiCodes(object):
def __init__(self):
# the subclasses declare class attributes which are numbers.
# Upon instantiation we define instance attributes, which are the same
# as the class attributes but wrapped with the ANSI escape sequence
for name in dir(self):
if not name.startswith("_"):
value = getattr(self, name)
setattr(self, name, code_to_chars(value))
class AnsiCursor(object):
def UP(self, n=1):
return CSI + str(n) + "A"
def DOWN(self, n=1):
return CSI + str(n) + "B"
def FORWARD(self, n=1):
return CSI + str(n) + "C"
def BACK(self, n=1):
return CSI + str(n) + "D"
def POS(self, x=1, y=1):
return CSI + str(y) + ";" + str(x) + "H"
class AnsiFore(AnsiCodes):
BLACK = 30
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
MAGENTA = 35
CYAN = 36
WHITE = 37
RESET = 39
# These are fairly well supported, but not part of the standard.
LIGHTBLACK_EX = 90
LIGHTRED_EX = 91
LIGHTGREEN_EX = 92
LIGHTYELLOW_EX = 93
LIGHTBLUE_EX = 94
LIGHTMAGENTA_EX = 95
LIGHTCYAN_EX = 96
LIGHTWHITE_EX = 97
class AnsiBack(AnsiCodes):
BLACK = 40
RED = 41
GREEN = 42
YELLOW = 43
BLUE = 44
MAGENTA = 45
CYAN = 46
WHITE = 47
RESET = 49
# These are fairly well supported, but not part of the standard.
LIGHTBLACK_EX = 100
LIGHTRED_EX = 101
LIGHTGREEN_EX = 102
LIGHTYELLOW_EX = 103
LIGHTBLUE_EX = 104
LIGHTMAGENTA_EX = 105
LIGHTCYAN_EX = 106
LIGHTWHITE_EX = 107
class AnsiStyle(AnsiCodes):
BRIGHT = 1
DIM = 2
NORMAL = 22
RESET_ALL = 0
Fore = AnsiFore()
Back = AnsiBack()
Style = AnsiStyle()
Cursor = AnsiCursor()
+46
View File
@@ -0,0 +1,46 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
from nemo.constants import NEMO_ENV_VARNAME_ENABLE_COLORING
from nemo.utils.env_var_parsing import get_envbool
__all__ = ["check_color_support", "to_unicode"]
def check_color_support():
# Colors can be forced with an env variable
if not sys.platform.lower().startswith("win") and get_envbool(NEMO_ENV_VARNAME_ENABLE_COLORING, False):
return True
def to_unicode(value):
"""
Converts a string argument to a unicode string.
If the argument is already a unicode string or None, it is returned
unchanged. Otherwise it must be a byte string and is decoded as utf8.
"""
try:
if isinstance(value, (str, type(None))):
return value
if not isinstance(value, bytes):
raise TypeError("Expected bytes, unicode, or None; got %r" % type(value))
return value.decode("utf-8")
except UnicodeDecodeError:
return repr(value)
+48
View File
@@ -0,0 +1,48 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from nemo.utils.env_var_parsing import get_envint
def is_global_rank_zero():
""" Helper function to determine if the current process is global_rank 0 (the main process)
"""
# Try to get the pytorch RANK env var
# RANK is set by torch.distributed.launch
rank = get_envint("RANK", None)
if rank:
return rank == 0
# If not set by pytorch, we need to determine node_rank
def get_node_rank():
# Use an equivalent of pytorch lightning's determine_ddp_node_rank()
node_rank = 0
# First check if running on a slurm cluster
# TODO: This check could probably be better
num_slurm_tasks = get_envint("SLURM_NTASKS", 0)
if num_slurm_tasks > 0:
node_rank = get_envint("SLURM_NODEID", 0)
else:
node_rank_env = get_envint("NODE_RANK", None)
group_rank = get_envint("GROUP_RANK", None)
if group_rank:
node_rank = group_rank
# Take from NODE_RANK whenever available
if node_rank_env:
node_rank = node_rank_env
return node_rank
node_rank = get_node_rank()
local_rank = get_envint("LOCAL_RANK", 0)
return node_rank == 0 and local_rank == 0
+58
View File
@@ -0,0 +1,58 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging as _logging
from logging.handlers import MemoryHandler
import pytorch_lightning as pl
HANDLERS = {}
PATCHED = False
def add_memory_handlers_to_pl_logger():
"""
Adds two MemoryHandlers to pytorch_lightning's logger. These two handlers are essentially message buffers. This
function is called in nemo.utils.__init__.py. These handlers are used in add_filehandlers_to_pl_logger to flush
buffered messages to files.
"""
if not HANDLERS:
HANDLERS["memory_err"] = MemoryHandler(-1)
HANDLERS["memory_err"].addFilter(lambda record: record.levelno > _logging.INFO)
HANDLERS["memory_all"] = MemoryHandler(-1)
pl._logger.addHandler(HANDLERS["memory_err"])
pl._logger.addHandler(HANDLERS["memory_all"])
def add_filehandlers_to_pl_logger(all_log_file, err_log_file):
"""
Adds two filehandlers to pytorch_lightning's logger. Called in nemo.utils.exp_manager(). The first filehandler
logs all messages to all_log_file while the second filehandler logs all WARNING and higher messages to err_log_file.
If "memory_err" and "memory_all" exist in HANDLERS, then those buffers are flushed to err_log_file and all_log_file
respectively, and then closed.
"""
HANDLERS["file"] = _logging.FileHandler(all_log_file)
pl._logger.addHandler(HANDLERS["file"])
HANDLERS["file_err"] = _logging.FileHandler(err_log_file)
HANDLERS["file_err"].addFilter(lambda record: record.levelno > _logging.INFO)
pl._logger.addHandler(HANDLERS["file_err"])
if HANDLERS.get("memory_all", None):
HANDLERS["memory_all"].setTarget(HANDLERS["file"])
HANDLERS["memory_all"].close()
del HANDLERS["memory_all"]
if HANDLERS.get("memory_err", None):
HANDLERS["memory_err"].setTarget(HANDLERS["file_err"])
HANDLERS["memory_err"].close()
del HANDLERS["memory_err"]
+39
View File
@@ -0,0 +1,39 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
class Singleton(type):
""" Implementation of a generic, tread-safe singleton meta-class.
Can be used as meta-class, i.e. will create
"""
# List of instances - one per class.
__instances = {}
# Lock used for accessing the instance.
__lock = threading.Lock()
def __call__(cls, *args, **kwargs):
""" Returns singleton instance. A thread safe implementation. """
if cls not in cls.__instances:
# Enter critical section.
with cls.__lock:
# Check once again.
if cls not in cls.__instances:
# Create a new object instance - one per class.
cls.__instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
# Return the instance.
return cls.__instances[cls]
+528
View File
@@ -0,0 +1,528 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import os
from dataclasses import dataclass, is_dataclass
from enum import Enum
from pathlib import Path
from typing import List, Optional, Union
import pytorch_lightning as pl
import wrapt
from omegaconf import DictConfig, ListConfig, OmegaConf
from omegaconf import errors as omegaconf_errors
from packaging import version
from nemo.utils import logging
_VAL_TEST_FASTPATH_KEY = 'ds_item'
class ArtifactPathType(Enum):
"""
ArtifactPathType refers to the type of the path that the artifact is located at.
LOCAL_PATH: A user local filepath that exists on the file system.
TAR_PATH: A (generally flattened) filepath that exists inside of an archive (that may have its own full path).
"""
LOCAL_PATH = 0
TAR_PATH = 1
@dataclass(init=False)
class ArtifactItem:
path: str
path_type: ArtifactPathType
hashed_path: Optional[str] = None
def resolve_dataset_name_from_cfg(cfg: DictConfig) -> str:
"""
Parses items of the provided sub-config to find the first potential key that
resolves to an existing file or directory.
# Fast-path Resolution
In order to handle cases where we need to resolve items that are not paths, a fastpath
key can be provided as defined in the global `_VAL_TEST_FASTPATH_KEY`.
This key can be used in two ways :
## _VAL_TEST_FASTPATH_KEY points to another key in the config
If this _VAL_TEST_FASTPATH_KEY points to another key in this config itself,
then we assume we want to loop through the values of that key.
This allows for any key in the config to become a fastpath key.
Example:
validation_ds:
splits: "val"
...
<_VAL_TEST_FASTPATH_KEY>: "splits" <-- this points to the key name "splits"
Then we can write the following when overriding in hydra:
```python
python train_file.py ... \
model.validation_ds.splits=[val1, val2, dev1, dev2] ...
```
## _VAL_TEST_FASTPATH_KEY itself acts as the resolved key
If this _VAL_TEST_FASTPATH_KEY does not point to another key in the config, then
it is assumed that the items of this key itself are used for resolution.
Example:
validation_ds:
...
<_VAL_TEST_FASTPATH_KEY>: "val" <-- this points to the key name "splits"
Then we can write the following when overriding in hydra:
```python
python train_file.py ... \
model.validation_ds.<_VAL_TEST_FASTPATH_KEY>=[val1, val2, dev1, dev2] ...
```
# IMPORTANT NOTE:
It <can> potentially mismatch if there exist more than 2 valid paths, and the
first path does *not* resolve the the path of the data file (but does resolve to
some other valid path).
To avoid this side-effect, place the data path as the first item on the config file.
Args:
cfg: DictConfig (Sub-config) that should be parsed.
Returns:
A str representing the `key` of the config which hosts the filepath(s),
or None in case path could not be resolved.
"""
if _VAL_TEST_FASTPATH_KEY in cfg and cfg[_VAL_TEST_FASTPATH_KEY] is not None:
fastpath_key = cfg[_VAL_TEST_FASTPATH_KEY]
if isinstance(fastpath_key, str) and fastpath_key in cfg:
return cfg[fastpath_key]
else:
return _VAL_TEST_FASTPATH_KEY
for key, value in cfg.items():
if type(value) in [list, tuple, ListConfig]:
# Count the number of valid paths in the list
values_are_paths = 0
for val_i in value:
val_i = str(val_i)
if os.path.exists(val_i) or os.path.isdir(val_i):
values_are_paths += 1
else:
# reset counter and break inner loop
break
if values_are_paths == len(value):
return key
else:
if os.path.exists(str(value)) or os.path.isdir(str(value)):
return key
return None
def parse_dataset_as_name(name: str) -> str:
"""
Constructs a valid prefix-name from a provided file path.
Args:
name: str path to some valid data/manifest file or a python object that
will be used as a name for the data loader (via str() cast).
Returns:
str prefix used to identify uniquely this data/manifest file.
"""
if os.path.exists(str(name)) or os.path.isdir(str(name)):
name = Path(name).stem
else:
name = str(name)
# cleanup name
name = name.replace('-', '_')
if 'manifest' in name:
name = name.replace('manifest', '')
if 'dataset' in name:
name = name.replace('dataset', '')
if '_' != name[-1]:
name = name + '_'
return name
def unique_names_check(name_list: Optional[List[str]]):
"""
Performs a uniqueness check on the name list resolved, so that it can warn users
about non-unique keys.
Args:
name_list: List of strings resolved for data loaders.
"""
if name_list is None:
return
# Name uniqueness checks
names = set()
for name in name_list:
if name in names:
logging.warning(
"Name resolution has found more than one data loader having the same name !\n"
"In such cases, logs will nor be properly generated. "
"Please rename the item to have unique names.\n"
f"Resolved name : {name}"
)
else:
names.add(name) # we need just hash key check, value is just a placeholder
def resolve_validation_dataloaders(model: 'ModelPT'):
"""
Helper method that operates on the ModelPT class to automatically support
multiple dataloaders for the validation set.
It does so by first resolving the path to one/more data files via `resolve_dataset_name_from_cfg()`.
If this resolution fails, it assumes the data loader is prepared to manually support / not support
multiple data loaders and simply calls the appropriate setup method.
If resolution succeeds:
Checks if provided path is to a single file or a list of files.
If a single file is provided, simply tags that file as such and loads it via the setup method.
If multiple files are provided:
Inject a new manifest path at index "i" into the resolved key.
Calls the appropriate setup method to set the data loader.
Collects the initialized data loader in a list and preserves it.
Once all data loaders are processed, assigns the list of loaded loaders to the ModelPT.
Finally assigns a list of unique names resolved from the file paths to the ModelPT.
Args:
model: ModelPT subclass, which requires >=1 Validation Dataloaders to be setup.
"""
cfg = copy.deepcopy(model._cfg)
dataloaders = []
# process val_loss_idx
if 'val_dl_idx' in cfg.validation_ds:
cfg = OmegaConf.to_container(cfg)
val_dl_idx = cfg['validation_ds'].pop('val_dl_idx')
cfg = OmegaConf.create(cfg)
else:
val_dl_idx = 0
# Set val_loss_idx
model._val_dl_idx = val_dl_idx
ds_key = resolve_dataset_name_from_cfg(cfg.validation_ds)
if ds_key is None:
logging.debug(
"Could not resolve file path from provided config - {}. "
"Disabling support for multi-dataloaders.".format(cfg.validation_ds)
)
model.setup_validation_data(cfg.validation_ds)
return
ds_values = cfg.validation_ds[ds_key]
if isinstance(ds_values, (list, tuple, ListConfig)):
for ds_value in ds_values:
cfg.validation_ds[ds_key] = ds_value
model.setup_validation_data(cfg.validation_ds)
dataloaders.append(model._validation_dl)
model._validation_dl = dataloaders
model._validation_names = [parse_dataset_as_name(ds) for ds in ds_values]
unique_names_check(name_list=model._validation_names)
return
else:
model.setup_validation_data(cfg.validation_ds)
model._validation_names = [parse_dataset_as_name(ds_values)]
unique_names_check(name_list=model._validation_names)
def resolve_test_dataloaders(model: 'ModelPT'):
"""
Helper method that operates on the ModelPT class to automatically support
multiple dataloaders for the test set.
It does so by first resolving the path to one/more data files via `resolve_dataset_name_from_cfg()`.
If this resolution fails, it assumes the data loader is prepared to manually support / not support
multiple data loaders and simply calls the appropriate setup method.
If resolution succeeds:
Checks if provided path is to a single file or a list of files.
If a single file is provided, simply tags that file as such and loads it via the setup method.
If multiple files are provided:
Inject a new manifest path at index "i" into the resolved key.
Calls the appropriate setup method to set the data loader.
Collects the initialized data loader in a list and preserves it.
Once all data loaders are processed, assigns the list of loaded loaders to the ModelPT.
Finally assigns a list of unique names resolved from the file paths to the ModelPT.
Args:
model: ModelPT subclass, which requires >=1 Test Dataloaders to be setup.
"""
cfg = copy.deepcopy(model._cfg)
dataloaders = []
# process test_loss_idx
if 'test_dl_idx' in cfg.test_ds:
cfg = OmegaConf.to_container(cfg)
test_dl_idx = cfg['test_ds'].pop('test_dl_idx')
cfg = OmegaConf.create(cfg)
else:
test_dl_idx = 0
# Set val_loss_idx
model._test_dl_idx = test_dl_idx
ds_key = resolve_dataset_name_from_cfg(cfg.test_ds)
if ds_key is None:
logging.debug(
"Could not resolve file path from provided config - {}. "
"Disabling support for multi-dataloaders.".format(cfg.test_ds)
)
model.setup_test_data(cfg.test_ds)
return
ds_values = cfg.test_ds[ds_key]
if isinstance(ds_values, (list, tuple, ListConfig)):
for ds_value in ds_values:
cfg.test_ds[ds_key] = ds_value
model.setup_test_data(cfg.test_ds)
dataloaders.append(model._test_dl)
model._test_dl = dataloaders
model._test_names = [parse_dataset_as_name(ds) for ds in ds_values]
unique_names_check(name_list=model._test_names)
return
else:
model.setup_test_data(cfg.test_ds)
model._test_names = [parse_dataset_as_name(ds_values)]
unique_names_check(name_list=model._test_names)
@wrapt.decorator
def wrap_training_step(wrapped, instance: pl.LightningModule, args, kwargs):
output_dict = wrapped(*args, **kwargs)
if isinstance(output_dict, dict) and output_dict is not None and 'log' in output_dict:
log_dict = output_dict.pop('log')
instance.log_dict(log_dict, on_step=True)
return output_dict
def convert_model_config_to_dict_config(cfg: Union[DictConfig, 'NemoConfig']) -> DictConfig:
"""
Converts its input into a standard DictConfig.
Possible input values are:
- DictConfig
- A dataclass which is a subclass of NemoConfig
Args:
cfg: A dict-like object.
Returns:
The equivalent DictConfig
"""
if not isinstance(cfg, (OmegaConf, DictConfig)) and is_dataclass(cfg):
cfg = OmegaConf.structured(cfg)
if not isinstance(cfg, DictConfig):
raise ValueError(f"cfg constructor argument must be of type DictConfig/dict but got {type(cfg)} instead.")
config = OmegaConf.to_container(cfg, resolve=True)
config = OmegaConf.create(config)
return config
def _convert_config(cfg: OmegaConf):
""" Recursive function convertint the configuration from old hydra format to the new one. """
# Get rid of cls -> _target_.
if 'cls' in cfg and '_target_' not in cfg:
cfg._target_ = cfg.pop('cls')
# Get rid of params.
if 'params' in cfg:
params = cfg.pop('params')
for param_key, param_val in params.items():
cfg[param_key] = param_val
# Recursion.
try:
for _, sub_cfg in cfg.items():
if isinstance(sub_cfg, DictConfig):
_convert_config(sub_cfg)
except omegaconf_errors.OmegaConfBaseException as e:
logging.warning(f"Skipped conversion for config/subconfig:\n{cfg}\n Reason: {e}.")
def maybe_update_config_version(cfg: DictConfig):
"""
Recursively convert Hydra 0.x configs to Hydra 1.x configs.
Changes include:
- `cls` -> `_target_`.
- `params` -> drop params and shift all arguments to parent.
- `target` -> `_target_` cannot be performed due to ModelPT injecting `target` inside class.
Args:
cfg: Any Hydra compatible DictConfig
Returns:
An updated DictConfig that conforms to Hydra 1.x format.
"""
if cfg is not None and not isinstance(cfg, DictConfig):
try:
temp_cfg = OmegaConf.create(cfg)
cfg = temp_cfg
except omegaconf_errors.OmegaConfBaseException:
# Cannot be cast to DictConfig, skip updating.
return cfg
# Make a copy of model config.
cfg = copy.deepcopy(cfg)
OmegaConf.set_struct(cfg, False)
# Convert config.
_convert_config(cfg)
# Update model config.
OmegaConf.set_struct(cfg, True)
return cfg
def import_class_by_path(path: str):
"""
Recursive import of class by path string.
"""
paths = path.split('.')
path = ".".join(paths[:-1])
class_name = paths[-1]
mod = __import__(path, fromlist=[class_name])
mod = getattr(mod, class_name)
return mod
def resolve_subclass_pretrained_model_info(base_class) -> List['PretrainedModelInfo']:
"""
Recursively traverses the inheritance graph of subclasses to extract all pretrained model info.
First constructs a set of unique pretrained model info by performing DFS over the inheritance graph.
All model info belonging to the same class is added together.
Args:
base_class: The root class, whose subclass graph will be traversed.
Returns:
A list of unique pretrained model infos belonging to all of the inherited subclasses of
this baseclass.
"""
list_of_models = set()
def recursive_subclass_walk(cls):
for subclass in cls.__subclasses__():
# step into its immediate subclass
recursive_subclass_walk(subclass)
subclass_models = subclass.list_available_models()
if subclass_models is not None and len(subclass_models) > 0:
# Inject subclass info into pretrained model info
# if not already overriden by subclass
for model_info in subclass_models:
# If subclass manually injects class_, dont override.
if model_info.class_ is None:
model_info.class_ = subclass
for model_info in subclass_models:
list_of_models.add(model_info)
recursive_subclass_walk(base_class)
list_of_models = list(sorted(list_of_models))
return list_of_models
def check_lib_version(lib_name: str, checked_version: str, operator) -> (Optional[bool], str):
"""
Checks if a library is installed, and if it is, checks the operator(lib.__version__, checked_version) as a result.
This bool result along with a string analysis of result is returned.
If the library is not installed at all, then returns None instead, along with a string explaining
that the library is not installed
Args:
lib_name: lower case str name of the library that must be imported.
checked_version: semver string that is compared against lib.__version__.
operator: binary callable function func(a, b) -> bool; that compares lib.__version__ against version in
some manner. Must return a boolean.
Returns:
A tuple of results:
- Bool or None. Bool if the library could be imported, and the result of
operator(lib.__version__, checked_version) or False if __version__ is not implemented in lib.
None is passed if the library is not installed at all.
- A string analysis of the check.
"""
try:
mod = __import__(lib_name)
if hasattr(mod, '__version__'):
lib_ver = version.Version(mod.__version__)
match_ver = version.Version(checked_version)
if operator(lib_ver, match_ver):
msg = f"Lib {lib_name} version is satisfied !"
return True, msg
else:
msg = (
f"Lib {lib_name} version ({lib_ver}) is not {operator.__name__} than required version {checked_version}.\n"
f"Please upgrade the lib using either pip or conda to the latest version."
)
return False, msg
else:
msg = (
f"Lib {lib_name} does not implement __version__ in its init file. "
f"Could not check version compatibility."
)
return False, msg
except (ImportError, ModuleNotFoundError):
pass
msg = f"Lib {lib_name} has not been installed. Please use pip or conda to install this package."
return None, msg
+421
View File
@@ -0,0 +1,421 @@
# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import enum
import logging as _logging
import sys
import threading
import warnings
from contextlib import contextmanager
from logging.handlers import MemoryHandler
from nemo.constants import NEMO_ENV_VARNAME_REDIRECT_LOGS_TO_STDERR, NEMO_ENV_VARNAME_TESTING
from nemo.utils.env_var_parsing import get_envbool
from nemo.utils.formatters.base import BaseNeMoFormatter, DebugNeMoFormatter
from nemo.utils.get_rank import is_global_rank_zero
from nemo.utils.metaclasses import Singleton
__all__ = ["Logger", "LogMode"]
class LogMode(enum.IntEnum):
EACH = 0 # Log the message each time
ONCE = 1 # Log the message only once. The same message will not be logged again.
class Logger(metaclass=Singleton):
# Level 0
NOTSET = _logging.NOTSET
# Level 10
DEBUG = _logging.DEBUG
# Level 20
INFO = _logging.INFO
# Level 30
WARNING = _logging.WARNING
# Level 40
ERROR = _logging.ERROR
# Level 50
CRITICAL = _logging.CRITICAL
_level_names = {
0: "NOTSET",
10: "DEBUG",
20: "INFO",
30: "WARNING",
40: "ERROR",
50: "CRITICAL",
}
def __init__(self, capture_warnings=True):
self._logger = None
# Multi-GPU runs run in separate processes, thread locks shouldn't be needed
self._logger_lock = threading.Lock()
self._handlers = dict()
self.old_warnings_showwarning = None
self._define_logger(capture_warnings)
self.once_logged = set()
self.rank = 0 if is_global_rank_zero() else "UNK"
def _define_logger(self, capture_warnings=True):
""" Creates the logger if not already created. Called in init"""
# Use double-checked locking to avoid taking lock unnecessarily.
if self._logger is not None:
return self._logger
with self._logger_lock:
try:
self._logger = _logging.getLogger("nemo_logger")
# By default, silence all loggers except the logger for rank 0
self.remove_stream_handlers()
# If NEMO_TESTING is set, add a streamhandler to all ranks
if get_envbool(NEMO_ENV_VARNAME_TESTING, False):
old_factory = _logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.rank = self.rank
return record
_logging.setLogRecordFactory(record_factory)
self.add_stream_handlers(formatter=DebugNeMoFormatter)
elif is_global_rank_zero():
self.add_stream_handlers()
# Add memoryhandlers, essentially buffers. They are used to save messages that we will flush to file
# once the appropriate file handlers are added.
if is_global_rank_zero():
# Add a memoryhandler for error messages. Only logged on rank 0
self._handlers["memory_err"] = MemoryHandler(-1)
self._handlers["memory_err"].addFilter(lambda record: record.levelno > _logging.INFO)
formatter = BaseNeMoFormatter
self._handlers["memory_err"].setFormatter(formatter())
self._logger.addHandler(self._handlers["memory_err"])
# Add a memoryhandler for all messages on all ranks
self._handlers["memory_all"] = MemoryHandler(-1)
formatter = BaseNeMoFormatter
self._handlers["memory_all"].setFormatter(formatter())
self._logger.addHandler(self._handlers["memory_all"])
finally:
level = Logger.INFO
if get_envbool(NEMO_ENV_VARNAME_TESTING, False):
level = Logger.DEBUG
self.set_verbosity(verbosity_level=level)
self.captureWarnings(capture_warnings)
self._logger.propagate = False
def remove_stream_handlers(self):
""" Removes StreamHandler that log to stdout and stderr from the logger."""
if self._logger is None:
raise RuntimeError("Impossible to set handlers if the Logger is not predefined")
# ======== Remove Handler if already existing ========
try:
self._logger.removeHandler(self._handlers["stream_stdout"])
del self._handlers["stream_stdout"]
except KeyError:
pass
try:
self._logger.removeHandler(self._handlers["stream_stderr"])
del self._handlers["stream_stderr"]
except KeyError:
pass
def add_stream_handlers(self, formatter=BaseNeMoFormatter):
"""Add StreamHandler that log to stdout and stderr to the logger. INFO and lower logs are streamed to stdout
while WARNING and higher are streamed to stderr. If the NEMO_ENV_VARNAME_REDIRECT_LOGS_TO_STDERR environment
variable is set, all logs are sent to stderr instead.
"""
if self._logger is None:
raise RuntimeError("Impossible to set handlers if the Logger is not predefined")
# Add the output handler.
if get_envbool(NEMO_ENV_VARNAME_REDIRECT_LOGS_TO_STDERR, False):
self._handlers["stream_stdout"] = _logging.StreamHandler(sys.stderr)
else:
self._handlers["stream_stdout"] = _logging.StreamHandler(sys.stdout)
self._handlers["stream_stdout"].addFilter(lambda record: record.levelno <= _logging.INFO)
self._handlers["stream_stderr"] = _logging.StreamHandler(sys.stderr)
self._handlers["stream_stderr"].addFilter(lambda record: record.levelno > _logging.INFO)
self._handlers["stream_stdout"].setFormatter(formatter())
self._logger.addHandler(self._handlers["stream_stdout"])
try:
self._handlers["stream_stderr"].setFormatter(formatter())
self._logger.addHandler(self._handlers["stream_stderr"])
except KeyError:
pass
def reset_stream_handler(self, formatter=BaseNeMoFormatter):
"""Removes then adds stream handlers."""
self.remove_stream_handlers()
self.add_stream_handlers(formatter=formatter)
def add_file_handler(self, log_file):
"""Add a FileHandler to logger that logs all messages to a file. If the logger had a MemoryHandler at
self._handlers["memory_all"], those buffered messages are flushed to the new file, and the MemoryHandler is
closed."""
if self._logger is None:
raise RuntimeError("Impossible to set handlers if the Logger is not predefined")
self._handlers["file"] = _logging.FileHandler(log_file)
formatter = BaseNeMoFormatter
self._handlers["file"].setFormatter(formatter())
self._logger.addHandler(self._handlers["file"])
if self._handlers.get("memory_all", None):
self._handlers["memory_all"].setTarget(self._handlers["file"])
self._handlers["memory_all"].close() # flush and remove
del self._handlers["memory_all"]
def add_err_file_handler(self, log_file):
"""Add a FileHandler to logger that logs all WARNING and higher messages to a file. If the logger had a
MemoryHandler at self._handlers["memory_err"], those buffered messages are flushed to the new file, and the
MemoryHandler is closed."""
if self._logger is None:
raise RuntimeError("Impossible to set handlers if the Logger is not predefined")
self._handlers["file_err"] = _logging.FileHandler(log_file)
self._handlers["file_err"].addFilter(lambda record: record.levelno > _logging.INFO)
formatter = BaseNeMoFormatter
self._handlers["file_err"].setFormatter(formatter())
self._logger.addHandler(self._handlers["file_err"])
if self._handlers.get("memory_err", None):
self._handlers["memory_err"].setTarget(self._handlers["file_err"])
self._handlers["memory_err"].close() # flush and remove
del self._handlers["memory_err"]
def getEffectiveLevel(self):
"""Return how much logging output will be produced."""
if self._logger is not None:
return self._logger.getEffectiveLevel()
def get_verbosity(self):
"""See getEffectiveLevel"""
return self.getEffectiveLevel()
def setLevel(self, verbosity_level):
"""Sets the threshold for what messages will be logged."""
if self._logger is not None:
self._logger.setLevel(verbosity_level)
for handler in self._logger.handlers:
handler.setLevel(verbosity_level)
def set_verbosity(self, verbosity_level):
"""See setLevel"""
self.setLevel(verbosity_level)
@contextmanager
def patch_stderr_handler(self, stream):
""" Sends messages that should log to stderr to stream instead. Useful for unittests """
if self._logger is not None:
try:
old_stream = self._handlers["stream_stderr"].stream
if old_stream is None:
raise ValueError
# Port backwards set_stream() from python 3.7
self._handlers["stream_stderr"].acquire()
try:
self._handlers["stream_stderr"].flush()
self._handlers["stream_stderr"].stream = stream
finally:
self._handlers["stream_stderr"].release()
yield stream
except (KeyError, ValueError):
raise RuntimeError("Impossible to patch logging handlers if handler does not exist")
finally:
# Port backwards set_stream() from python 3.7
self._handlers["stream_stderr"].acquire()
try:
self._handlers["stream_stderr"].flush()
self._handlers["stream_stderr"].stream = old_stream
finally:
self._handlers["stream_stderr"].release()
else:
raise RuntimeError("Impossible to patch logging handlers if handler does not exist")
@contextmanager
def patch_stdout_handler(self, stream):
""" Sends messages that should log to stdout to stream instead. Useful for unittests """
if self._logger is not None:
try:
old_stream = self._handlers["stream_stdout"].stream
if old_stream is None:
raise ValueError
# Port backwards set_stream() from python 3.7
self._handlers["stream_stdout"].acquire()
try:
self._handlers["stream_stdout"].flush()
self._handlers["stream_stdout"].stream = stream
finally:
self._handlers["stream_stdout"].release()
yield stream
except (KeyError, ValueError):
raise RuntimeError("Impossible to patch logging handlers if handler does not exist")
finally:
# Port backwards set_stream() from python 3.7
self._handlers["stream_stdout"].acquire()
try:
self._handlers["stream_stdout"].flush()
self._handlers["stream_stdout"].stream = old_stream
finally:
self._handlers["stream_stdout"].release()
else:
raise RuntimeError("Impossible to patch logging handlers if handler does not exist")
@contextmanager
def temp_verbosity(self, verbosity_level):
"""Sets the a temporary threshold for what messages will be logged."""
if self._logger is not None:
old_verbosity = self.get_verbosity()
try:
self.set_verbosity(verbosity_level)
yield
finally:
self.set_verbosity(old_verbosity)
else:
try:
yield
finally:
pass
def captureWarnings(self, capture):
"""
If capture is true, redirect all warnings to the logging package.
If capture is False, ensure that warnings are not redirected to logging
but to their original destinations.
"""
if self._logger is not None:
if capture and self.old_warnings_showwarning is None:
# Backup Method
self.old_warnings_showwarning = warnings.showwarning
warnings.showwarning = self._showwarning
elif not capture and self.old_warnings_showwarning is not None:
# Restore Method
warnings.showwarning = self.old_warnings_showwarning
self.old_warnings_showwarning = None
def _showwarning(self, message, category, filename, lineno, file=None, line=None):
"""
Implementation of showwarnings which redirects to logging.
It will call warnings.formatwarning and will log the resulting string
with level logging.WARNING.
"""
s = warnings.formatwarning(message, category, filename, lineno, line)
self.warning("%s", s)
def _logged_once(self, msg, mode):
PREFIX_LEN = 12
if mode == LogMode.ONCE:
if msg[PREFIX_LEN:] in self.once_logged:
return True
self.once_logged.add(msg[PREFIX_LEN:])
return False
def debug(self, msg, *args, mode=LogMode.EACH, **kwargs):
"""
Log 'msg % args' with severity 'DEBUG'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
"""
if self._logger is not None and self._logger.isEnabledFor(Logger.DEBUG) and not self._logged_once(msg, mode):
self._logger._log(Logger.DEBUG, msg, args, **kwargs)
def info(self, msg, *args, mode=LogMode.EACH, **kwargs):
"""
Log 'msg % args' with severity 'INFO'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.info("Houston, we have a %s", "interesting problem", exc_info=1)
"""
if self._logger is not None and self._logger.isEnabledFor(Logger.INFO) and not self._logged_once(msg, mode):
self._logger._log(Logger.INFO, msg, args, **kwargs)
def warning(self, msg, *args, mode=LogMode.EACH, **kwargs):
"""
Log 'msg % args' with severity 'WARNING'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.warning("Houston, we have a %s", "bit of a problem", exc_info=1)
"""
if self._logger is not None and self._logger.isEnabledFor(Logger.WARNING) and not self._logged_once(msg, mode):
self._logger._log(Logger.WARNING, msg, args, **kwargs)
def error(self, msg, *args, mode=LogMode.EACH, **kwargs):
"""
Log 'msg % args' with severity 'ERROR'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.error("Houston, we have a %s", "major problem", exc_info=1)
"""
if self._logger is not None and self._logger.isEnabledFor(Logger.ERROR) and not self._logged_once(msg, mode):
self._logger._log(Logger.ERROR, msg, args, **kwargs)
def critical(self, msg, *args, mode=LogMode.EACH, **kwargs):
"""
Log 'msg % args' with severity 'CRITICAL'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.critical("Houston, we have a %s", "major disaster", exc_info=1)
"""
if (
self._logger is not None
and self._logger.isEnabledFor(Logger.CRITICAL)
and not self._logged_once(msg, mode)
):
self._logger._log(Logger.CRITICAL, msg, args, **kwargs)