latest (dev)
Copy
Latest development documentation · Updated 2026-10-08
Source code for tensorplay.ao.pruning.base_classes
"""Base classes shared by every pruning method.
A pruning method reparameterizes one named tensor of a module: the original
values move to a parameter called ``<name>_orig``, a binary mask is stored in
a buffer called ``<name>_mask``, and the attribute ``<name>`` holds their
elementwise product. A forward pre-hook recomputes that product before every
forward pass, so gradients keep flowing into the original values while the
masked entries stay at zero.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Any
import tensorplay
from tensorplay import Tensor, nn
__all__ = ["PruningBase", "BasePruningMethod", "PruningContainer"]
[docs]
class BasePruningMethod(ABC):
"""Abstract base class for pruning techniques.
Subclasses must override :meth:`compute_mask` and declare a
``PRUNING_TYPE`` class attribute (one of ``'unstructured'``,
``'structured'`` or ``'global'``). The classmethod :meth:`apply` installs
the reparameterization on a module and registers an instance of the
subclass as a forward pre-hook.
"""
_tensor_name: str
def __call__(self, module: nn.Module, inputs: Any) -> None:
"""Recompute the pruned tensor as the mask times the original values.
Called as a forward pre-hook: multiplies the mask stored in
``module[name + '_mask']`` into the original values stored in
``module[name + '_orig']`` and stores the product into
``module[name]``.
Args:
module: module holding the pruned tensor.
inputs: unused; present for hook-signature compatibility.
"""
setattr(module, self._tensor_name, self.apply_mask(module))
[docs]
@abstractmethod
def compute_mask(self, t: Tensor, default_mask: Tensor) -> Tensor:
"""Compute the pruning mask for the input tensor ``t``.
Starting from ``default_mask`` (a mask of ones when ``t`` has never
been pruned), derive the new mask according to the recipe of the
concrete method. Entries already zeroed by ``default_mask`` must stay
zero.
Args:
t: tensor whose entries or channels are ranked for pruning,
typically the importance scores of the parameter.
default_mask: mask accumulated from previous pruning iterations;
must be respected by the new mask. Same shape as ``t``.
Returns:
The mask to apply to ``t``, with the same shape as ``t``.
"""
[docs]
def apply_mask(self, module: nn.Module) -> Tensor:
"""Return the pruned version of the tensor held by ``module``.
Fetches the mask and the original values from the module and returns
their elementwise product.
Args:
module: module holding the pruned tensor.
Returns:
The product of the mask and the original values.
"""
# the mask can only be multiplied once it has been computed, so the
# method must know which tensor it operates on; this attribute is set
# by apply()
if self._tensor_name is None:
raise AssertionError(
f"Module {module} has to be pruned"
) # this gets set in apply()
mask = getattr(module, self._tensor_name + "_mask")
orig = getattr(module, self._tensor_name + "_orig")
pruned_tensor = mask.to(dtype=orig.dtype) * orig
return pruned_tensor
[docs]
@classmethod
def apply(
cls,
module: nn.Module,
name: str,
*args: Any,
importance_scores: Tensor | None = None,
**kwargs: Any,
) -> "BasePruningMethod":
"""Install the pruning reparameterization for ``module[name]``.
Moves the original parameter to ``name + '_orig'``, registers the
computed mask as the buffer ``name + '_mask'``, stores the masked
values under ``name`` and adds a forward pre-hook that re-applies the
mask on every forward pass. If the tensor is already pruned by
another method, the new method is composed with the existing one
through a :class:`PruningContainer`.
Args:
module: module containing the tensor to prune.
name: parameter name within ``module`` on which pruning acts.
args: positional arguments forwarded to the subclass constructor.
importance_scores: tensor of importance scores with the same
shape as ``module[name]``; each entry ranks the corresponding
element of the parameter. When unspecified, the parameter
itself is used as its own importance scores.
kwargs: keyword arguments forwarded to the subclass constructor.
Returns:
The pruning method (or container of methods) now attached to the
module.
"""
def _get_composite_method(
cls, module: nn.Module, name: str, *args: Any, **kwargs: Any
) -> "BasePruningMethod":
# Check if a pruning method has already been applied to
# `module[name]`. If so, store that in `old_method`.
old_method = None
found = 0
# there should technically be only 1 hook with hook._tensor_name
# == name; assert this using `found`
hooks_to_remove = []
for k, hook in module._forward_pre_hooks.items():
# if it exists, take existing thing, remove hook, then
# go through normal thing
if isinstance(hook, BasePruningMethod) and hook._tensor_name == name:
old_method = hook
hooks_to_remove.append(k)
found += 1
if found > 1:
raise AssertionError(
f"Avoid adding multiple pruning hooks to the "
f"same tensor {name} of module {module}. Use a PruningContainer."
)
for k in hooks_to_remove:
del module._forward_pre_hooks[k]
# Apply the new pruning method, either from scratch or on top of
# the previous one.
method = cls(*args, **kwargs) # new pruning
# Have the pruning method remember what tensor it's been applied to
method._tensor_name = name
# combine `methods` with `old_method`, if `old_method` exists
if old_method is not None: # meaning that there was a hook
# if the hook is already a pruning container, just add the
# new pruning method to the container
if isinstance(old_method, PruningContainer):
old_method.add_pruning_method(method)
method = old_method # rename old_method --> method
# if the hook is simply a single pruning method, create a
# container, add the old pruning method and the new one
elif isinstance(old_method, BasePruningMethod):
container = PruningContainer(old_method)
container.add_pruning_method(method)
method = container # rename container --> method
return method
method = _get_composite_method(cls, module, name, *args, **kwargs)
# at this point we have no forward_pre_hooks but we could have an
# active reparameterization of the tensor if another pruning method
# had been applied (in which case `method` would be a PruningContainer
# and not a simple pruning method).
# Pruning is to be applied to the module's tensor named `name`,
# starting from the state it is found in prior to this iteration of
# pruning. The pruning mask is calculated based on importance scores.
orig = getattr(module, name)
if importance_scores is not None:
if importance_scores.shape != orig.shape:
raise AssertionError(
f"importance_scores should have the same shape as parameter "
f"{name} of {module}, got {importance_scores.shape} vs {orig.shape}"
)
else:
importance_scores = orig
# If this is the first time pruning is applied, take care of moving
# the original tensor to a new parameter called name + '_orig'
# and deleting the original parameter
if not isinstance(method, PruningContainer):
# copy `module[name]` to `module[name + '_orig']`
module.register_parameter(name + "_orig", orig)
# temporarily delete `module[name]`
del module._parameters[name]
default_mask = tensorplay.ones_like(orig) # temp
# If this is not the first time pruning is applied, all of the above
# has been done before in a previous pruning iteration, so we're good
# to go
else:
default_mask = (
getattr(module, name + "_mask")
.detach()
.clone(memory_format=tensorplay.contiguous_format)
)
# Use try/except because if anything goes wrong with the mask
# computation etc., you'd want to roll back.
try:
# get the final mask, computed according to the specific method
mask = method.compute_mask(importance_scores, default_mask=default_mask)
# reparameterize by saving mask to `module[name + '_mask']`...
module.register_buffer(name + "_mask", mask)
# ... and the new pruned tensor to `module[name]`
setattr(module, name, method.apply_mask(module))
# associate the pruning method to the module via a hook to
# compute the function before every forward() (compile by run)
module.register_forward_pre_hook(method)
except Exception as e:
if not isinstance(method, PruningContainer):
orig = getattr(module, name + "_orig")
module.register_parameter(name, orig)
del module._parameters[name + "_orig"]
raise e
return method
[docs]
def prune(
self,
t: Tensor,
default_mask: Tensor | None = None,
importance_scores: Tensor | None = None,
) -> Tensor:
"""Return a pruned copy of the input tensor ``t``.
Applies the rule implemented by :meth:`compute_mask` without any
module-side reparameterization.
Args:
t: tensor to prune (same shape as ``default_mask``).
importance_scores: tensor of importance scores with the same
shape as ``t``; each entry ranks the corresponding element of
``t``. When unspecified, ``t`` itself is used.
default_mask: mask from a previous pruning iteration, if any.
Pruning must respect the entries it already zeroes. When
unspecified, a mask of ones is used.
Returns:
The pruned version of ``t``.
"""
if importance_scores is not None:
if importance_scores.shape != t.shape:
raise AssertionError(
f"importance_scores should have the same shape as tensor t, "
f"got {importance_scores.shape} vs {t.shape}"
)
else:
importance_scores = t
default_mask = default_mask if default_mask is not None else tensorplay.ones_like(t)
return t * self.compute_mask(importance_scores, default_mask=default_mask)
[docs]
def remove(self, module: nn.Module) -> None:
"""Make the current pruning of ``module`` permanent.
The pruned values remain pruned: the product of mask and original
values is written back into the parameter ``name``, and the auxiliary
parameter ``name + '_orig'`` and buffer ``name + '_mask'`` are
dropped.
Note:
Pruning itself is NOT undone or reversed!
"""
# before removing pruning from a tensor, it has to have been applied
if self._tensor_name is None:
raise AssertionError(
f"Module {module} has to be pruned before pruning can be removed"
) # this gets set in apply()
# to update module[name] to latest trained weights
weight = self.apply_mask(module) # masked weights
# delete and reset
if hasattr(module, self._tensor_name):
delattr(module, self._tensor_name)
orig = module._parameters[self._tensor_name + "_orig"]
orig.data = weight.data
del module._parameters[self._tensor_name + "_orig"]
del module._buffers[self._tensor_name + "_mask"]
setattr(module, self._tensor_name, orig)
[docs]
class PruningContainer(BasePruningMethod):
"""Sequence of pruning methods applied iteratively to the same tensor.
Tracks the order in which methods were added and combines successive
pruning calls: each new method only ranks the entries or channels that
the previous masks left unpruned.
Accepts as argument an instance of a :class:`BasePruningMethod` or an
iterable of them.
"""
def __init__(self, *args: BasePruningMethod) -> None:
self._pruning_methods: tuple[BasePruningMethod, ...] = ()
if not isinstance(args, Iterable): # only 1 item
self._tensor_name = args._tensor_name # type: ignore[attr-defined]
self.add_pruning_method(args) # type: ignore[arg-type]
elif len(args) == 1: # only 1 item in a tuple
self._tensor_name = args[0]._tensor_name
self.add_pruning_method(args[0])
else: # manual construction from list or other iterable (or no args)
for method in args:
self.add_pruning_method(method)
[docs]
def add_pruning_method(self, method: BasePruningMethod | None) -> None:
"""Add a child pruning ``method`` to the container.
Args:
method: child pruning method to be added to the container.
Raises:
TypeError: if ``method`` is neither ``None`` nor a
:class:`BasePruningMethod` instance.
ValueError: if ``method`` acts on a different tensor name than
the methods already held by the container.
"""
# check that we're adding a pruning method to the container
if not isinstance(method, BasePruningMethod) and method is not None:
raise TypeError(f"{type(method)} is not a BasePruningMethod subclass")
elif method is not None and self._tensor_name != method._tensor_name:
raise ValueError(
"Can only add pruning methods acting on "
f"the parameter named '{self._tensor_name}' to PruningContainer {self}."
+ f" Found '{method._tensor_name}'"
)
# if all checks passed, add to _pruning_methods tuple
self._pruning_methods += (method,) # type: ignore[operator]
def __len__(self) -> int:
return len(self._pruning_methods)
def __iter__(self):
return iter(self._pruning_methods)
def __getitem__(self, idx):
return self._pruning_methods[idx]
[docs]
def compute_mask(self, t: Tensor, default_mask: Tensor) -> Tensor:
"""Apply the latest method and merge its mask into ``default_mask``.
The new partial mask is computed on the entries or channels that
``default_mask`` has not zeroed out. Which portion of ``t`` the new
mask is derived from depends on the ``PRUNING_TYPE`` of the last
method:
* ``'unstructured'``: the mask is computed from the flattened list of
entries not yet masked;
* ``'structured'``: the mask is computed from the channels that still
hold at least one unmasked entry;
* ``'global'``: the mask is computed across all entries.
Args:
t: tensor representing the parameter to prune (same shape as
``default_mask``).
default_mask: mask accumulated from previous pruning iterations.
Returns:
The mask combining the effects of ``default_mask`` and of the
latest method, with the same shape as ``default_mask`` and ``t``.
"""
def _combine_masks(method: BasePruningMethod, t: Tensor, mask: Tensor) -> Tensor:
"""Combine the mask of one method with a pre-existing ``mask``.
Args:
method: pruning method currently being applied.
t: tensor representing the parameter to prune (same shape as
``mask``).
mask: mask accumulated from previous pruning iterations.
Returns:
The mask combining the effects of the old mask and of the
current pruning method (same shape as ``mask`` and ``t``).
"""
new_mask = mask # start off from existing mask
new_mask = new_mask.to(dtype=t.dtype)
# compute a slice of t onto which the new pruning method will operate
if method.PRUNING_TYPE == "unstructured":
# prune entries of t where the mask is 1
slc = mask == 1
# for struct pruning, exclude channels that have already been
# entirely pruned
elif method.PRUNING_TYPE == "structured":
if not hasattr(method, "dim"):
raise AttributeError(
"Pruning methods of PRUNING_TYPE "
'"structured" need to have the attribute `dim` defined.'
)
# find the channels to keep by removing the ones that have been
# zeroed out already (i.e. where sum(entries) == 0)
n_dims = t.dim() # "is this a 2D tensor? 3D? ..."
dim = method.dim
# convert negative indexing
if dim < 0:
dim = n_dims + dim
# if dim is still negative after subtracting it from n_dims
if dim < 0:
raise IndexError(
f"Index is out of bounds for tensor with dimensions {n_dims}"
)
# find channels along dim = dim that aren't already tots 0ed out
keep_channel = mask.sum(dim=[d for d in range(n_dims) if d != dim]) != 0
# create slice to identify what to prune
slc: Any = [slice(None)] * n_dims
slc[dim] = keep_channel
elif method.PRUNING_TYPE == "global":
n_dims = len(t.shape) # "is this a 2D tensor? 3D? ..."
slc = [slice(None)] * n_dims
else:
raise ValueError(f"Unrecognized PRUNING_TYPE {method.PRUNING_TYPE}")
# compute the new mask on the unpruned slice of the tensor t
if isinstance(slc, list):
slc = tuple(slc)
partial_mask = method.compute_mask(t[slc], default_mask=mask[slc])
new_mask[slc] = partial_mask.to(dtype=new_mask.dtype)
return new_mask
method = self._pruning_methods[-1]
mask = _combine_masks(method, t, default_mask)
return mask
#: Alias of :class:`BasePruningMethod` kept for naming convenience.
PruningBase = BasePruningMethodHelp improve this page
Found an error, an unclear step, or a missing example?
Was this page helpful?

