chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# 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 hydra
import lightning.pytorch
import omegaconf
from nemo.core.classes.common import (
FileIO,
Model,
PretrainedModelInfo,
Serialization,
Typing,
is_typecheck_enabled,
typecheck,
)
from nemo.core.classes.dataset import Dataset, IterableDataset
from nemo.core.classes.exportable import Exportable, ExportFormat
from nemo.core.classes.loss import Loss
from nemo.core.classes.mixins import access_mixins, adapter_mixins, hf_io_mixin
from nemo.core.classes.modelPT import ModelPT
from nemo.core.classes.module import NeuralModule
from nemo.utils import exceptions
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
# 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 typing import Optional
from torch.utils import data
from nemo.core.classes import Serialization, Typing, typecheck
__all__ = ['Dataset', 'IterableDataset']
class Dataset(data.Dataset, Typing, Serialization):
"""Dataset with output ports
Please Note: Subclasses of IterableDataset should *not* implement input_types.
"""
def _collate_fn(self, batch):
"""
A default implementation of a collation function.
Users should override this method to define custom data loaders.
"""
return data.dataloader.default_collate(batch)
@typecheck()
def collate_fn(self, batch):
"""
This is the method that user pass as functor to DataLoader.
The method optionally performs neural type checking and add types to the outputs.
Please note, subclasses of Dataset should not implement `input_types`.
Usage:
.. code-block:: python
dataloader = torch.utils.data.DataLoader(
....,
collate_fn=dataset.collate_fn,
....
)
Returns:
Collated batch, with or without types.
"""
if self.input_types is not None:
raise TypeError("Datasets should not implement `input_types` as they are not checked")
# Simply forward the inner `_collate_fn`
return self._collate_fn(batch)
class IterableDataset(data.IterableDataset, Typing, Serialization):
"""Iterable Dataset with output ports
Please Note: Subclasses of IterableDataset should *not* implement input_types.
"""
def _collate_fn(self, batch):
"""
A default implementation of a collation function.
Users should override this method to define custom data loaders.
"""
return data.dataloader.default_collate(batch)
@typecheck()
def collate_fn(self, batch):
"""
This is the method that user pass as functor to DataLoader.
The method optionally performs neural type checking and add types to the outputs.
# Usage:
dataloader = torch.utils.data.DataLoader(
....,
collate_fn=dataset.collate_fn,
....
)
Returns:
Collated batch, with or without types.
"""
if self.input_types is not None:
raise TypeError("Datasets should not implement `input_types` as they are not checked")
# Simply forward the inner `_collate_fn`
return self._collate_fn(batch)
@dataclass
class DatasetConfig:
# ...
batch_size: int = 32
drop_last: bool = False
shuffle: bool = False
num_workers: Optional[int] = 0
pin_memory: bool = True
+370
View File
@@ -0,0 +1,370 @@
# 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 abc import ABC
from typing import Dict, List, Optional, Union
import torch
from lightning.pytorch.core.module import _jit_is_scripting
from nemo.core.classes import typecheck
from nemo.core.neural_types import NeuralType
from nemo.core.utils.neural_type_utils import get_dynamic_axes, get_io_names
from nemo.utils import logging, monkeypatched
from nemo.utils.export_utils import (
ExportFormat,
augment_filename,
get_export_format,
parse_input_example,
rename_onnx_io,
replace_for_export,
verify_runtime,
verify_torchscript,
wrap_forward_method,
)
__all__ = ['ExportFormat', 'Exportable']
class Exportable(ABC):
"""
This Interface should be implemented by particular classes derived from nemo.core.NeuralModule or nemo.core.ModelPT.
It gives these entities ability to be exported for deployment to formats such as ONNX.
Usage:
# exporting pre-trained model to ONNX file for deployment.
model.eval()
model.to('cuda') # or to('cpu') if you don't have GPU
model.export('mymodel.onnx', [options]) # all arguments apart from `output` are optional.
"""
@property
def input_module(self):
return self
@property
def output_module(self):
return self
def export(
self,
output: str,
input_example=None,
verbose=False,
do_constant_folding=True,
onnx_opset_version=None,
check_trace: Union[bool, List[torch.Tensor]] = False,
dynamic_axes=None,
check_tolerance=0.01,
export_modules_as_functions=False,
keep_initializers_as_inputs=None,
use_dynamo=False,
):
"""
Exports the model to the specified format. The format is inferred from the file extension of the output file.
Args:
output (str): Output file name. File extension be .onnx, .pt, or .ts, and is used to select export
path of the model.
input_example (list or dict): Example input to the model's forward function. This is used to
trace the model and export it to ONNX/TorchScript. If the model takes multiple inputs, then input_example
should be a list of input examples. If the model takes named inputs, then input_example
should be a dictionary of input examples.
verbose (bool): If True, will print out a detailed description of the model's export steps, along with
the internal trace logs of the export process.
do_constant_folding (bool): If True, will execute constant folding optimization on the model's graph
before exporting. This is ONNX specific.
onnx_opset_version (int): The ONNX opset version to export the model to. If None, will use a reasonable
default version.
check_trace (bool): If True, will verify that the model's output matches the output of the traced
model, upto some tolerance.
dynamic_axes (dict): A dictionary mapping input and output names to their dynamic axes. This is
used to specify the dynamic axes of the model's inputs and outputs. If the model takes multiple inputs,
then dynamic_axes should be a list of dictionaries. If the model takes named inputs, then dynamic_axes
should be a dictionary of dictionaries. If None, will use the dynamic axes of the input_example
derived from the NeuralType of the input and output of the model.
check_tolerance (float): The tolerance to use when checking the model's output against the traced
model's output. This is only used if check_trace is True. Note the high tolerance is used because
the traced model is not guaranteed to be 100% accurate.
export_modules_as_functions (bool): If True, will export the model's submodules as functions. This is
ONNX specific.
keep_initializers_as_inputs (bool): If True, will keep the model's initializers as inputs in the onnx graph.
This is ONNX specific.
use_dynamo (bool): If True, use onnx.dynamo_export() instead of onnx.export(). This is ONNX specific.
Returns:
A tuple of two outputs.
Item 0 in the output is a list of outputs, the outputs of each subnet exported.
Item 1 in the output is a list of string descriptions. The description of each subnet exported can be
used for logging purposes.
"""
all_out = []
all_descr = []
for subnet_name in self.list_export_subnets():
model = self.get_export_subnet(subnet_name)
out_name = augment_filename(output, subnet_name)
out, descr, out_example = model._export(
out_name,
input_example=input_example,
verbose=verbose,
do_constant_folding=do_constant_folding,
onnx_opset_version=onnx_opset_version,
check_trace=check_trace,
dynamic_axes=dynamic_axes,
check_tolerance=check_tolerance,
export_modules_as_functions=export_modules_as_functions,
keep_initializers_as_inputs=keep_initializers_as_inputs,
use_dynamo=use_dynamo,
)
# Propagate input example (default scenario, may need to be overriden)
if input_example is not None:
input_example = out_example
all_out.append(out)
all_descr.append(descr)
logging.info("Successfully exported {} to {}".format(model.__class__.__name__, out_name))
return (all_out, all_descr)
def _export(
self,
output: str,
input_example=None,
verbose=False,
do_constant_folding=True,
onnx_opset_version=None,
check_trace: Union[bool, List[torch.Tensor]] = False,
dynamic_axes=None,
check_tolerance=0.01,
export_modules_as_functions=False,
keep_initializers_as_inputs=None,
use_dynamo=False,
):
my_args = locals().copy()
my_args.pop('self')
self.eval()
for param in self.parameters():
param.requires_grad = False
exportables = []
for m in self.modules():
if isinstance(m, Exportable):
exportables.append(m)
qual_name = self.__module__ + '.' + self.__class__.__qualname__
format = get_export_format(output)
output_descr = f"{qual_name} exported to {format}"
# Pytorch's default opset version is too low, using reasonable latest one
if onnx_opset_version is None:
onnx_opset_version = 17
try:
# Disable typechecks
typecheck.set_typecheck_enabled(enabled=False)
# Allow user to completely override forward method to export
forward_method, old_forward_method = wrap_forward_method(self)
# Set module mode
with torch.inference_mode(), torch.no_grad(), torch.jit.optimized_execution(True), _jit_is_scripting():
if input_example is None:
input_example = self.input_module.input_example()
# Remove i/o examples from args we propagate to enclosed Exportables
my_args.pop('output')
my_args.pop('input_example')
# Run (posibly overridden) prepare methods before calling forward()
for ex in exportables:
ex._prepare_for_export(**my_args, noreplace=True)
self._prepare_for_export(output=output, input_example=input_example, **my_args)
input_list, input_dict = parse_input_example(input_example)
input_names = self.input_names
output_names = self.output_names
output_example = self.forward(*input_list, **input_dict)
if not isinstance(output_example, tuple):
output_example = (output_example,)
if check_trace:
if isinstance(check_trace, bool):
check_trace_input = [input_example]
else:
check_trace_input = check_trace
if format == ExportFormat.TORCHSCRIPT:
jitted_model = torch.jit.trace_module(
self,
{"forward": tuple(input_list) + tuple(input_dict.values())},
strict=True,
check_trace=check_trace,
check_tolerance=check_tolerance,
)
jitted_model = torch.jit.freeze(jitted_model)
if verbose:
logging.info(f"JIT code:\n{jitted_model.code}")
jitted_model.save(output)
jitted_model = torch.jit.load(output)
if check_trace:
verify_torchscript(jitted_model, output, check_trace_input, check_tolerance)
elif format == ExportFormat.ONNX:
# dynamic axis is a mapping from input/output_name => list of "dynamic" indices
if dynamic_axes is None:
dynamic_axes = self.dynamic_shapes_for_export(use_dynamo)
# When using dynamo export, use dynamic_shapes instead of dynamic_axes
if use_dynamo:
dynamic_shapes = dynamic_axes
if use_dynamo:
typecheck.enable_wrapping(enabled=False)
# https://github.com/pytorch/pytorch/issues/126339
with monkeypatched(torch.nn.RNNBase, "flatten_parameters", lambda *args: None):
logging.info(f"Running export.export, dynamic shapes:{dynamic_shapes}\n")
# We have to use different types of arguments for dynamo_export to achieve
# same external weights behaviour as onnx.export :
# https://github.com/pytorch/pytorch/issues/126479
# https://github.com/pytorch/pytorch/issues/126269
mem_params = sum([param.nelement() * param.element_size() for param in self.parameters()])
mem_bufs = sum([buf.nelement() * buf.element_size() for buf in self.buffers()])
mem = mem_params + mem_bufs
if mem > 2 * 1000 * 1000 * 1000:
ex_model = torch.export.export(
self,
tuple(input_list),
kwargs=input_dict,
dynamic_shapes=dynamic_shapes,
strict=False,
)
ex_model = ex_model.run_decompositions()
model_state = ex_model.state_dict
else:
model_state = None
ex_model = self
options = torch.onnx.ExportOptions(dynamic_shapes=True, op_level_debug=True)
ex = torch.onnx.dynamo_export(ex_model, *input_list, **input_dict, export_options=options)
ex.save(output, model_state=model_state)
del ex
del ex_model
# Rename I/O after save - don't want to risk modifying ex._model_proto
rename_onnx_io(output, input_names, output_names)
else:
torch.onnx.export(
self,
input_example,
output,
input_names=input_names,
output_names=output_names,
verbose=verbose,
do_constant_folding=do_constant_folding,
dynamic_axes=dynamic_axes,
opset_version=onnx_opset_version,
keep_initializers_as_inputs=keep_initializers_as_inputs,
export_modules_as_functions=export_modules_as_functions,
dynamo=False, # Use legacy TorchScript-based exporter for LSTM compatibility
)
if check_trace:
verify_runtime(self, output, check_trace_input, input_names, check_tolerance=check_tolerance)
else:
raise ValueError(f'Encountered unknown export format {format}.')
finally:
typecheck.enable_wrapping(enabled=True)
typecheck.set_typecheck_enabled(enabled=True)
if forward_method:
type(self).forward = old_forward_method
self._export_teardown()
return (output, output_descr, output_example)
@property
def disabled_deployment_input_names(self) -> List[str]:
"""Implement this method to return a set of input names disabled for export"""
return []
@property
def disabled_deployment_output_names(self) -> List[str]:
"""Implement this method to return a set of output names disabled for export"""
return []
@property
def supported_export_formats(self) -> List[ExportFormat]:
"""Implement this method to return a set of export formats supported. Default is all types."""
return [ExportFormat.ONNX, ExportFormat.TORCHSCRIPT]
def _prepare_for_export(self, **kwargs):
"""
Override this method to prepare module for export. This is in-place operation.
Base version does common necessary module replacements (Apex etc)
"""
if not 'noreplace' in kwargs:
replace_for_export(self)
def _export_teardown(self):
"""
Override this method for any teardown code after export.
"""
pass
@property
def input_names(self):
return get_io_names(self.input_module.input_types_for_export, self.disabled_deployment_input_names)
@property
def output_names(self):
return get_io_names(self.output_module.output_types_for_export, self.disabled_deployment_output_names)
@property
def input_types_for_export(self) -> Optional[Dict[str, NeuralType]]:
return self.input_types
@property
def output_types_for_export(self):
return self.output_types
def dynamic_shapes_for_export(self, use_dynamo=False):
return get_dynamic_axes(self.input_module.input_types_for_export, self.input_names, use_dynamo)
def get_export_subnet(self, subnet=None):
"""
Returns Exportable subnet model/module to export
"""
if subnet is None or subnet == 'self':
return self
else:
return getattr(self, subnet)
def list_export_subnets(self):
"""
Returns default set of subnet names exported for this model
First goes the one receiving input (input_example)
"""
return ['self']
def get_export_config(self):
"""
Returns export_config dictionary
"""
return getattr(self, 'export_config', {})
def set_export_config(self, args):
"""
Sets/updates export_config dictionary
"""
ex_config = self.get_export_config()
ex_config.update(args)
self.export_config = ex_config
+26
View File
@@ -0,0 +1,26 @@
# 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 torch
from nemo.core.classes.common import Serialization, Typing
__all__ = ['Loss']
class Loss(torch.nn.modules.loss._Loss, Typing, Serialization):
"""Inherit this class to implement custom loss."""
def __init__(self, **kwargs):
super(Loss, self).__init__(**kwargs)
+28
View File
@@ -0,0 +1,28 @@
# Copyright (c) 2022, 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.core.classes.mixins.access_mixins import AccessMixin, set_access_cfg
from nemo.core.classes.mixins.adapter_mixin_strategies import (
ResidualAddAdapterStrategy,
ResidualAddAdapterStrategyConfig,
ReturnResultAdapterStrategy,
ReturnResultAdapterStrategyConfig,
)
from nemo.core.classes.mixins.adapter_mixins import (
AdapterModelPTMixin,
AdapterModuleMixin,
get_registered_adapter,
register_adapter,
)
from nemo.core.classes.mixins.hf_io_mixin import HuggingFaceFileIO
+146
View File
@@ -0,0 +1,146 @@
# 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 abc import ABC
from typing import Optional
import torch
from omegaconf import DictConfig
_DEFAULT_ACCESS_GUID = "default"
_ACCESS_CFG = DictConfig({_DEFAULT_ACCESS_GUID: {"detach": False, "convert_to_cpu": False}})
_ACCESS_ENABLED = {_DEFAULT_ACCESS_GUID: False}
def set_access_cfg(cfg: DictConfig, guid: Optional[str] = None):
if cfg is None or not isinstance(cfg, DictConfig):
raise TypeError(f"cfg must be a DictConfig")
global _ACCESS_CFG
global _DEFAULT_ACCESS_GUID
if guid is not None:
_ACCESS_CFG[guid] = cfg
else:
_ACCESS_CFG[_DEFAULT_ACCESS_GUID] = cfg
class AccessMixin(ABC):
"""
Allows access to output of intermediate layers of a model
"""
def __init__(self):
super().__init__()
self._registry = {} # dictionary of lists
def register_accessible_tensor(self, name, tensor):
"""
Register tensor for later use.
"""
if self.access_cfg.get('convert_to_cpu', False):
tensor = tensor.cpu()
if self.access_cfg.get('detach', False):
tensor = tensor.detach()
if not hasattr(self, '_registry'):
self._registry = {}
if name not in self._registry:
self._registry[name] = []
self._registry[name].append(tensor)
@classmethod
def get_module_registry(cls, module: torch.nn.Module):
"""
Extract all registries from named submodules, return dictionary where
the keys are the flattened module names, the values are the internal registry
of each such module.
"""
module_registry = {}
for name, m in module.named_modules():
if hasattr(m, '_registry') and len(m._registry) > 0:
module_registry[name] = m._registry
return module_registry
def reset_registry(self: torch.nn.Module, registry_key: Optional[str] = None):
"""
Reset the registries of all named sub-modules
"""
if hasattr(self, "_registry"):
if registry_key is None:
self._registry.clear()
else:
if registry_key in self._registry:
self._registry.pop(registry_key)
else:
raise KeyError(
f"Registry key `{registry_key}` provided, but registry does not have this key.\n"
f"Available keys in registry : {list(self._registry.keys())}"
)
for _, m in self.named_modules():
if hasattr(m, "_registry"):
if registry_key is None:
m._registry.clear()
else:
if registry_key in self._registry:
self._registry.pop(registry_key)
else:
raise KeyError(
f"Registry key `{registry_key}` provided, but registry does not have this key.\n"
f"Available keys in registry : {list(self._registry.keys())}"
)
# Explicitly disable registry cache after reset
AccessMixin.set_access_enabled(access_enabled=False, guid=getattr(self, "model_guid", None))
@property
def access_cfg(self):
"""
Returns:
The global access config shared across all access mixin modules.
"""
global _ACCESS_CFG
global _DEFAULT_ACCESS_GUID
guid = self.model_guid if getattr(self, "model_guid", None) else _DEFAULT_ACCESS_GUID
if hasattr(self, "propagate_model_guid"):
self.propagate_model_guid()
if guid not in _ACCESS_CFG:
_ACCESS_CFG[guid] = DictConfig({})
return _ACCESS_CFG[guid]
@classmethod
def update_access_cfg(cls, cfg: dict, guid: Optional[str] = None):
global _ACCESS_CFG
global _DEFAULT_ACCESS_GUID
guid = guid if guid is not None else _DEFAULT_ACCESS_GUID
if guid not in _ACCESS_CFG:
_ACCESS_CFG[guid] = cfg
else:
_ACCESS_CFG[guid].update(cfg)
@classmethod
def is_access_enabled(cls, guid: Optional[str] = None):
global _ACCESS_ENABLED
global _DEFAULT_ACCESS_GUID
guid = guid if guid is not None else _DEFAULT_ACCESS_GUID
return _ACCESS_ENABLED.get(guid, False)
@classmethod
def set_access_enabled(cls, access_enabled: bool, guid: Optional[str] = None):
global _ACCESS_ENABLED
global _DEFAULT_ACCESS_GUID
guid = guid if guid is not None else _DEFAULT_ACCESS_GUID
_ACCESS_ENABLED[guid] = access_enabled
@@ -0,0 +1,259 @@
# Copyright (c) 2022, 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 abc import ABC
from dataclasses import dataclass
from typing import Any, Dict, List, Tuple, Union
import torch
from nemo.core.classes.mixins import AccessMixin
class AbstractAdapterStrategy(ABC):
def forward(self, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'):
"""
Forward method that defines how the output of the adapter should be merged with the input, or if it
should be merged at all.
Also provides the module that called this strategy - thereby allowing access to all other
adapters in the calling module. This can be useful if one adapter is a meta adapter, that
combines the outputs of various adapters. In such a case, the input can be forwarded across
all other adapters, collecting their outputs, and those outputs can then be merged via some
strategy. For example, refer to :
- [AdapterFusion: Non-Destructive Task Composition for Transfer Learning](https://arxiv.org/abs/2005.00247)
- [Exploiting Adapters for Cross-lingual Low-resource Speech Recognition](https://arxiv.org/abs/2105.11905)
Args:
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after one of the active adapters has finished its forward passes.
"""
raise NotImplementedError()
def __call__(self, *args, **kwargs):
return self.forward(*args, **kwargs)
class ReturnResultAdapterStrategy(AbstractAdapterStrategy):
"""
An implementation of an adapter strategy that simply returns the result of the adapter.
Supports stochastic
"""
def forward(self, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'):
"""
A basic strategy, which simply returns the result of the adapter's calculation as the output.
Args:
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after one of the active adapters has finished its forward passes.
"""
result = self.compute_output(input, adapter, module=module)
return result
def compute_output(
self,
input: Union[torch.Tensor, List[torch.Tensor], Tuple[torch.Tensor], Dict[str, Any]],
adapter: torch.nn.Module,
*,
module: 'AdapterModuleMixin',
) -> torch.Tensor:
"""
Compute the output of a single adapter to some input.
Args:
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after one of the active adapters has finished its forward passes.
"""
if isinstance(input, (list, tuple)):
out = adapter(*input)
elif isinstance(input, dict):
out = adapter(**input)
else:
out = adapter(input)
return out
@dataclass
class ReturnResultAdapterStrategyConfig:
_target_: str = "{0}.{1}".format(
ReturnResultAdapterStrategy.__module__, ReturnResultAdapterStrategy.__name__
) # mandatory field
class ResidualAddAdapterStrategy(AbstractAdapterStrategy):
"""
An implementation of residual addition of an adapter module with its input.
Supports stochastic depth regularization.
"""
def __init__(self, stochastic_depth: float = 0.0, l2_lambda: float = 0.0):
"""
An implementation of residual addition of an adapter module with its input.
Performs output = input + adapter(input).
Args:
stochastic_depth: float, when greater than one, can optionally dropout the output of
the adapter's forward pass.
l2_lambda: L2 norm of the difference between the original input to the function, and the adapter's
output result. Disabled if set to 0.0.
"""
super().__init__()
self.stochastic_depth = stochastic_depth
self.l2_lambda = l2_lambda
def forward(self, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'):
"""
A basic strategy, comprising of a residual connection over the input, after forward pass by
the underlying adapter.
Args:
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after one of the active adapters has finished its forward passes.
"""
out = self.compute_output(input, adapter, module=module)
# If not in training mode, or probability of stochastic depth is 0, skip step.
p = self.stochastic_depth
if not module.training or p == 0.0:
pass
else:
out = self.apply_stochastic_depth(out, input, adapter, module=module)
# Return the residual connection output = input + adapter(input)
result = input + out
# If l2_lambda is activated, register the loss value
self.compute_auxiliary_losses(result, input, adapter, module=module)
return result
def compute_output(
self, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'
) -> torch.Tensor:
"""
Compute the output of a single adapter to some input.
Args:
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after one of the active adapters has finished its forward passes.
"""
out = adapter(input)
return out
def apply_stochastic_depth(
self, output: torch.Tensor, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'
):
"""
Compute and apply stochastic depth if probability is greater than 0.
Args:
output: The result tensor, after one of the active adapters has finished its forward passes.
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
Returns:
The result tensor, after stochastic depth has been potentially applied to it.
"""
# Perform stochastic depth if needed.
p = self.stochastic_depth
if p < 0.0 or p > 1.0:
raise ValueError(f"Stochastic depth probability has to be between 0 and 1, but got {p}")
# Apply stochastic depth to the output of adapter.
keep_prob = 1.0 - p
shape = [1] * output.ndim
noise = torch.empty(shape, dtype=output.dtype, device=output.device)
noise = noise.bernoulli_(keep_prob)
if keep_prob > 0.0: # Done to normalize activation for inference mode
noise.div_(keep_prob)
output = noise * output
return output
def compute_auxiliary_losses(
self, output: torch.Tensor, input: torch.Tensor, adapter: torch.nn.Module, *, module: 'AdapterModuleMixin'
):
"""
Compute any auxiliary losses and preserve it in the tensor registry.
Args:
output: The result tensor, after one of the active adapters has finished its forward passes.
input: Original output tensor of the module, or the output of the previous adapter (if more than
one adapters are enabled).
adapter: The adapter module that is currently required to perform the forward pass.
module: The calling module, in its entirety. It is a module that implements `AdapterModuleMixin`,
therefore the strategy can access all other adapters in this module via `module.adapter_layer`.
"""
if module.training and self.l2_lambda > 0.0:
if not isinstance(adapter, AccessMixin):
raise ValueError(f"Module {adapter.__class__.__name__} does not implement AccessMixin !")
# Only add auxiliary loss if adapter has trainable parameters that require gradients
if next(adapter.parameters()).requires_grad is True:
# Check if globally allowed to compute aux loss
compute_aux_loss = adapter.access_cfg.get('compute_adapter_loss', True)
if compute_aux_loss:
# if l2 lambda is enabled, also enable AccessMixin
adapter.set_access_enabled(access_enabled=True, guid=getattr(self, "model_guid", None))
l2_loss = self.l2_lambda * (input - output).square().reshape(input.size(0), -1).sum(dim=-1).mean()
adapter.register_accessible_tensor(name='adapter_loss', tensor=l2_loss)
@dataclass
class ResidualAddAdapterStrategyConfig:
stochastic_depth: float = 0.0
l2_lambda: float = 0.0
_target_: str = "{0}.{1}".format(
ResidualAddAdapterStrategy.__module__, ResidualAddAdapterStrategy.__name__
) # mandatory field
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
# Copyright (c) 2025, 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 abc import ABC
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Union
from huggingface_hub import HfApi, ModelCard, ModelCardData
from huggingface_hub import get_token as get_hf_token
from huggingface_hub.hf_api import ModelInfo
from huggingface_hub.utils import SoftTemporaryDirectory
class HuggingFaceFileIO(ABC):
"""
Mixin that provides Hugging Face file IO functionality for NeMo models.
It is usually implemented as a mixin to `ModelPT`.
This mixin provides the following functionality:
- `search_huggingface_models()`: Search the hub programmatically via some model filter.
- `push_to_hf_hub()`: Push a model to the hub.
"""
@classmethod
def get_hf_model_filter(cls) -> Dict[str, Any]:
"""
Generates a filter for HuggingFace models.
Additionaly includes default values of some metadata about results returned by the Hub.
Metadata:
resolve_card_info: Bool flag, if set, returns the model card metadata. Default: False.
limit_results: Optional int, limits the number of results returned.
Returns:
A dict representing the arguments passable to huggingface list_models().
"""
model_filter = dict(
author=None,
filter=['nemo'],
model_name=None,
limit=None,
full=None,
cardData=False,
)
return model_filter
@classmethod
def search_huggingface_models(cls, model_filter: Optional[Dict[str, Any]] = None) -> Iterable['ModelInfo']:
"""
Should list all pre-trained models available via Hugging Face Hub.
The following metadata can be passed via the `model_filter` for additional results.
Metadata:
resolve_card_info: Bool flag, if set, returns the model card metadata. Default: False.
limit_results: Optional int, limits the number of results returned.
.. code-block:: python
# You can replace <DomainSubclass> with any subclass of ModelPT.
from nemo.core import ModelPT
# Get default filter dict
filt = <DomainSubclass>.get_hf_model_filter()
# Make any modifications to the filter as necessary
filt['filter'].append('en') # Add language filter
filt['filter'].append('automatic-speech-recognition') # Add task filter
# Add any metadata to the filter as needed (kwargs to list_models)
filt['limit'] = 5
# Obtain model info
model_infos = <DomainSubclass>.search_huggingface_models(model_filter=filt)
# Browse through cards and select an appropriate one
card = model_infos[0]
# Restore model using `modelId` of the card.
model = ModelPT.from_pretrained(card.modelId)
Args:
model_filter: Optional Dictionary (for Hugging Face Hub kwargs)
that filters the returned list of compatible model cards, and selects all results from each filter.
Users can then use `model_card.modelId` in `from_pretrained()` to restore a NeMo Model.
Returns:
A list of ModelInfo entries.
"""
# Resolve model filter if not provided as argument
if model_filter is None:
model_filter = cls.get_hf_model_filter()
# Check if api token exists, use if it does
hf_token = get_hf_token()
# Search for all valid models after filtering
api = HfApi()
results = api.list_models(token=hf_token, sort="lastModified", **model_filter) # type: Iterable[ModelInfo]
return results
def push_to_hf_hub(
self,
repo_id: str,
*,
pack_nemo_file: bool = True,
model_card: Optional['ModelCard'] | object | str = None,
commit_message: str = "Push model using huggingface_hub.",
private: bool = False,
api_endpoint: Optional[str] = None,
token: Optional[str] = None,
branch: Optional[str] = None,
allow_patterns: Optional[Union[List[str], str]] = None,
ignore_patterns: Optional[Union[List[str], str]] = None,
delete_patterns: Optional[Union[List[str], str]] = None,
):
"""
Upload model checkpoint to the Hub.
Use `allow_patterns` and `ignore_patterns` to precisely filter which files should be pushed to the hub. Use
`delete_patterns` to delete existing remote files in the same commit. See [`upload_folder`] reference for more
details.
Args:
repo_id (`str`):
ID of the repository to push to (example: `"username/my-model"`).
pack_nemo_file (`bool`, *optional*, defaults to `True`): Whether to pack the model checkpoint and
configuration into a single `.nemo` file. If set to false, uploads the contents of the directory
containing the model checkpoint and configuration plus additional artifacts.
model_card (`ModelCard`, *optional*): Model card to upload with the model. If None, will use the model
card template provided by the class itself via `generate_model_card()`. Any object that implements
str(obj) can be passed here. Two keyword replacements are passed to `generate_model_card()`:
`model_name` and `repo_id`. If the model card generates a string, and it contains `{model_name}` or
`{repo_id}`, they will be replaced with the actual values.
commit_message (`str`, *optional*):
Message to commit while pushing.
private (`bool`, *optional*, defaults to `False`):
Whether the repository created should be private.
api_endpoint (`str`, *optional*):
The API endpoint to use when pushing the model to the hub.
token (`str`, *optional*):
The token to use as HTTP bearer authorization for remote files. By default, it will use the token
cached when running `huggingface-cli login`.
branch (`str`, *optional*):
The git branch on which to push the model. This defaults to `"main"`.
allow_patterns (`List[str]` or `str`, *optional*):
If provided, only files matching at least one pattern are pushed.
ignore_patterns (`List[str]` or `str`, *optional*):
If provided, files matching any of the patterns are not pushed.
delete_patterns (`List[str]` or `str`, *optional*):
If provided, remote files matching any of the patterns will be deleted from the repo.
Returns:
The url of the uploaded HF repo.
"""
if "/" not in repo_id or len(repo_id.split("/")) != 2:
raise ValueError("Invalid repo_id provided. Please provide a repo_id of the form `username/repo-name`.")
domain_name, model_name = repo_id.split("/")
if token is None:
token = get_hf_token()
api = HfApi(endpoint=api_endpoint, token=token)
repo_id = api.create_repo(repo_id=repo_id, private=private, exist_ok=True).repo_id
# Push the files to the repo in a single commit
with SoftTemporaryDirectory() as tmp:
saved_path = Path(tmp) / repo_id
saved_path.mkdir(parents=True, exist_ok=True)
# Save nemo file in temp dir
# Get SaveRestoreConnector from subclass implementation
if not hasattr(self, '_save_restore_connector'):
raise NotImplementedError(
"Model must implement a `_save_restore_connector` property to push to the HuggingFace Hub."
)
# We want to save a NeMo file, but not pack its contents into a tarfile by default
save_restore_connector = self._save_restore_connector
save_restore_connector.pack_nemo_file = pack_nemo_file
nemo_filepath = saved_path / f"{model_name}.nemo"
self.save_to(nemo_filepath)
# Save model card in temp dir
if model_card is None:
card_model_name = model_name.replace("_", " ").split(" ")
card_model_name = " ".join([word.capitalize() for word in card_model_name])
template_kwargs = {
'model_name': card_model_name,
'repo_id': repo_id,
}
# Generate model card from subclass that implements this method
model_card = self.generate_model_card(type='hf', template_kwargs=template_kwargs)
# Convert model card to str
model_card = str(model_card)
# Write model card to temp dir
model_card_filepath = saved_path / "README.md"
model_card_filepath.write_text(str(model_card), encoding='utf-8', errors='ignore')
api.upload_folder(
repo_id=repo_id,
repo_type="model",
folder_path=saved_path,
commit_message=commit_message,
revision=branch,
allow_patterns=allow_patterns,
ignore_patterns=ignore_patterns,
delete_patterns=delete_patterns,
)
if branch is None:
branch = "main"
return f"https://huggingface.co/{repo_id}/tree/{branch}"
def _get_hf_model_card(self, template: str, template_kwargs: Optional[Dict[str, str]] = None):
"""
Generate a HuggingFace ModelCard from a str template. The template may have markers with `{key}` that will be
populated by values from `template_kwargs` if provided.
Args:
template: Str template for the model card.
template_kwargs (optional): Dict of key-value pairs to populate the template with.
Returns:
A HuggingFace ModelCard object that can be converted to a model card string.
"""
card_data = ModelCardData(
library_name='nemo',
tags=['pytorch', 'NeMo'],
license='cc-by-4.0',
ignore_metadata_errors=True,
)
if 'card_data' not in template_kwargs:
template_kwargs['card_data'] = card_data.to_yaml()
# Update template with kwargs
# We need to do a manual replace because not all keys may be provided in the kwargs
for key, val in template_kwargs.items():
template = template.replace("{" + key.strip() + "}", val)
hf_model_card = ModelCard(template)
return hf_model_card
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
# 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 contextlib import contextmanager
import torch
from torch.nn import Module
from nemo.core.classes.common import FileIO, Serialization, Typing
from nemo.utils import logging
__all__ = ['NeuralModule', 'freeze', 'unfreeze']
def freeze(module: Module) -> None:
"""Freeze all parameters of ``module`` and snapshot their prior ``requires_grad`` state.
The snapshot is stored on ``module._frozen_grad_map`` so a later call to ``unfreeze(..., partial=True)``
can restore the pre-freeze state instead of unconditionally enabling gradients.
"""
grad_map = {pname: param.requires_grad for pname, param in module.named_parameters()}
for param in module.parameters():
param.requires_grad = False
if not hasattr(module, '_frozen_grad_map'):
module._frozen_grad_map = grad_map
else:
module._frozen_grad_map.update(grad_map)
module.eval()
def unfreeze(module: Module, partial: bool = False) -> None:
"""Unfreeze parameters of ``module``.
If ``partial=True``, restore each parameter's ``requires_grad`` from the snapshot recorded by
``freeze(module)``; otherwise enable gradients on every parameter. The snapshot is cleared in
both cases and ``module.train()`` is called.
"""
if partial and not hasattr(module, '_frozen_grad_map'):
raise ValueError("Cannot unfreeze partially without first freezing the module with `freeze()`")
for pname, param in module.named_parameters():
if not partial:
param.requires_grad = True
elif pname in module._frozen_grad_map:
param.requires_grad = module._frozen_grad_map[pname]
else:
logging.warning(
f"Parameter {pname} not found in list of previously frozen parameters. Unfreezing this parameter."
)
param.requires_grad = True
if hasattr(module, '_frozen_grad_map'):
delattr(module, '_frozen_grad_map')
module.train()
class NeuralModule(Module, Typing, Serialization, FileIO):
"""
Abstract class offering interface shared between all PyTorch Neural Modules.
"""
@property
def num_weights(self):
"""
Utility property that returns the total number of parameters of NeuralModule.
"""
return self._num_weights()
@torch.jit.ignore
def _num_weights(self):
num: int = 0
for p in self.parameters():
if p.requires_grad:
num += p.numel()
return num
def input_example(self, max_batch=None, max_dim=None):
"""
Override this method if random inputs won't work
Returns:
A tuple sample of valid input data.
"""
return None
def freeze(self) -> None:
r"""Freeze all params for inference. See :func:`freeze` for details."""
freeze(self)
def unfreeze(self, partial: bool = False) -> None:
"""Unfreeze parameters for training. See :func:`unfreeze` for details.
Example:
```python
model.encoder.freeze() # caller freezes encoder
model.freeze() # freezes everything; encoder snapshot preserved
model.unfreeze(partial=True) # decoder unfrozen, encoder stays frozen
```
"""
unfreeze(self, partial=partial)
@contextmanager
def as_frozen(self):
"""
Context manager which temporarily freezes a module, yields control and finally unfreezes the module partially
to return to original state.
Allows for either total unfreeze or partial unfreeze (if the module was explicitly frozen
previously with `freeze()`). The `partial` argument is used to determine whether to unfreeze
all parameters or only the parameters that were previously unfrozen prior `freeze()`.
Example:
with model.as_frozen(): # by default, partial = True
# Do something with the model
pass
# Model's parameters are now back to original state of requires_grad
"""
training_mode = self.training
self.freeze()
try:
yield
finally:
self.unfreeze(partial=True)
if training_mode:
self.train()
else:
self.eval()