latest (dev)
Copy
Latest development documentation · Updated 2026-10-08
Source code for tensorplay.ao.quantization.fuse_modules
"""Module fusion for inference and quantization-aware training.
Fuses supported patterns — convolution or linear followed by batch norm and
an optional relu — by folding the batch-norm affine transform into the
preceding operator's weights. Folding is exact in inference (eval) mode:
conv(x; W, b) followed by BN(gamma, beta, mean, var) with std =
sqrt(var + eps) computes gamma * (conv_out - mean) / std + beta, which
equals conv(x; W', b') with W' = W * gamma/std and
b' = gamma * (b - mean)/std + beta.
"""
from __future__ import annotations
import tensorplay
from tensorplay import nn
__all__ = ["fuse_modules", "fuse_known_modules", "fuse_conv_bn_weights"]
def fuse_conv_bn_weights(conv_w, conv_b, bn_mean, bn_var, bn_gamma, bn_beta, eps):
"""Fold a batch norm affine transform into convolution/linear weights.
Returns the folded weight and bias (as a plain tensor or ``None`` when
the input bias is ``None`` and the folded bias is exactly the batch norm
shift).
"""
std = (bn_var + eps).sqrt()
scale = bn_gamma / std
# one broadcastable 1 after the channel axis, then one per spatial dim
reshaped = scale.reshape([-1, 1] + [1] * (conv_w.dim() - 2))
fused_w = conv_w * reshaped
shift = bn_beta - bn_gamma * bn_mean / std
if conv_b is not None:
fused_b = conv_b * scale + shift
else:
fused_b = shift
return fused_w, fused_b
_FUSED_MODULE_FOR = {}
def _register_fused_modules():
from tensorplay.ao.nn.intrinsic import ConvReLU1d, ConvReLU2d, ConvReLU3d, LinearReLU
_FUSED_MODULE_FOR.update({
(nn.Conv1d, nn.ReLU): ConvReLU1d,
(nn.Conv2d, nn.ReLU): ConvReLU2d,
(nn.Conv3d, nn.ReLU): ConvReLU3d,
(nn.Linear, nn.ReLU): LinearReLU,
})
def _get_supported_fusion_patterns() -> dict:
return {
(nn.Conv1d, nn.BatchNorm1d): nn.Conv1d,
(nn.Conv1d, nn.BatchNorm1d, nn.ReLU): ConvReLU1d_holder(),
(nn.Conv2d, nn.BatchNorm2d): nn.Conv2d,
(nn.Conv2d, nn.BatchNorm2d, nn.ReLU): ConvReLU2d_holder(),
(nn.Conv3d, nn.BatchNorm3d): nn.Conv3d,
(nn.Conv3d, nn.BatchNorm3d, nn.ReLU): ConvReLU3d_holder(),
(nn.Linear, nn.BatchNorm1d): nn.Linear,
(nn.Linear, nn.BatchNorm1d, nn.ReLU): LinearReLU_holder(),
(nn.Linear, nn.ReLU): LinearReLU_holder(),
(nn.Conv1d, nn.ReLU): ConvReLU1d_holder(),
(nn.Conv2d, nn.ReLU): ConvReLU2d_holder(),
(nn.Conv3d, nn.ReLU): ConvReLU3d_holder(),
}
def ConvReLU1d_holder():
from tensorplay.ao.nn.intrinsic import ConvReLU1d
return ConvReLU1d
def ConvReLU2d_holder():
from tensorplay.ao.nn.intrinsic import ConvReLU2d
return ConvReLU2d
def ConvReLU3d_holder():
from tensorplay.ao.nn.intrinsic import ConvReLU3d
return ConvReLU3d
def LinearReLU_holder():
from tensorplay.ao.nn.intrinsic import LinearReLU
return LinearReLU
def _resolve_path(root, path):
module = root
for atom in path.split("."):
module = module._modules[atom]
return module
def _set_path(root, path, module):
atoms = path.split(".")
parent = root
for atom in atoms[:-1]:
parent = parent._modules[atom]
parent._modules[atoms[-1]] = module
def fuse_known_modules(modules):
"""Fuse one pattern group given the resolved module instances.
Returns the fused module, or ``None`` when the pattern is unsupported.
"""
if len(modules) == 1:
return modules[0]
base = modules[0]
act = modules[-1] if isinstance(modules[-1], nn.ReLU) else None
body = modules[:-1] if act is not None else modules
bn = body[1] if len(body) > 1 else None
if bn is not None:
if isinstance(base, (nn.Conv1d, nn.Conv2d, nn.Conv3d)):
expected_bn = {nn.Conv1d: nn.BatchNorm1d, nn.Conv2d: nn.BatchNorm2d,
nn.Conv3d: nn.BatchNorm3d}[type(base)]
elif isinstance(base, nn.Linear):
expected_bn = nn.BatchNorm1d
else:
return None
if not isinstance(bn, expected_bn):
return None
fused_w, fused_b = fuse_conv_bn_weights(
base.weight.detach(),
base.bias.detach() if base.bias is not None else None,
bn.running_mean.detach(),
bn.running_var.detach(),
bn.weight.detach() if bn.weight is not None else None,
bn.bias.detach() if bn.bias is not None else None,
bn.eps,
)
fused = _make_base(base, fused_w, fused_b)
else:
fused = _make_base(base, base.weight.detach(),
base.bias.detach() if base.bias is not None else None)
if act is not None:
_register_fused_modules()
fused_cls = _FUSED_MODULE_FOR.get((type(base), nn.ReLU))
if fused_cls is not None:
fused = fused_cls.from_float(fused)
return fused
def _make_base(base, weight, bias):
if isinstance(base, (nn.Conv1d, nn.Conv2d, nn.Conv3d)):
fused = type(base)(
base.in_channels,
base.out_channels,
base.kernel_size,
base.stride,
base.padding,
base.dilation,
base.groups,
bias is not None,
)
else:
fused = nn.Linear(base.in_features, base.out_features, bias is not None)
with tensorplay.no_grad():
fused.weight.copy_(weight)
if bias is not None:
fused.bias.copy_(bias)
fused.train(base.training)
return fused
def _should_fuse(group, patterns) -> bool:
types = tuple(type(m) for m in group)
return types in patterns
[docs]
def fuse_modules(model, modules_to_fuse, inplace=False, fuser_func=None):
"""Fuse supported modules in-place within a model.
Args:
model: the model to fuse.
modules_to_fuse: an iterable of module-name groups, each a list of
fully-qualified attribute paths, e.g.
``[["conv1", "bn1", "relu1"], ["fc"]]``. Groups of length one
are ignored (kept for call-site symmetry).
inplace: mutate the model instead of returning a copy.
fuser_func: replacement for :func:`fuse_known_modules`.
Returns:
The fused model (the same object when ``inplace``).
"""
import copy
if fuser_func is None:
fuser_func = fuse_known_modules
if not inplace:
model = copy.deepcopy(model)
patterns = _get_supported_fusion_patterns()
for group_names in modules_to_fuse:
if len(group_names) <= 1:
continue
group = [_resolve_path(model, name) for name in group_names]
if not _should_fuse(group, patterns):
raise ValueError(
f"fuse_modules: unsupported fusion pattern "
f"{[type(m).__name__ for m in group]} for "
f"{list(group_names)}")
fused = fuser_func(group)
if fused is None:
raise ValueError(
f"fuse_modules: failed to fuse {list(group_names)}")
_set_path(model, group_names[0], fused)
for name in group_names[1:]:
atoms = name.split(".")
parent = model
for atom in atoms[:-1]:
parent = parent._modules[atom]
del parent._modules[atoms[-1]]
return modelHelp improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

