mirror of
https://github.com/storytold/storyteller-ml.git
synced 2026-10-13 23:53:02 +00:00
226 lines
8.1 KiB
Python
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)
|