Files
storyteller-ml/tts/nemo/utils/export_utils.py
T
2022-02-28 05:43:54 -05:00

226 lines
8.1 KiB
Python

# 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)