from __future__ import annotations
import math
from typing import (
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
TYPE_CHECKING,
Tuple,
Type,
Union,
)
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import fx
from torch.nn.utils.fusion import fuse_conv_bn_eval
from tqdm import tqdm
from spikingjelly.activation_based import neuron
from spikingjelly.activation_based.ann2snn.factories import HookFactory, NeuronFactory
from spikingjelly.activation_based.ann2snn.modules import (
ChannelVoltageScaler,
_safe_quantile,
)
from spikingjelly.activation_based.ann2snn.rules import ActivationRule, ReLURule
from spikingjelly.activation_based.ann2snn.threshold import ThresholdOptimizer
from spikingjelly.activation_based.ann2snn.recipes.base import ConversionRecipe
from spikingjelly.activation_based.ann2snn.recipes.step_mode_adapters import (
_RATE_CODING_SAFE_MODULE_TYPES,
_RATE_CODING_STATELESS_MODULE_TYPES,
adapt_step_mode_graph,
)
if TYPE_CHECKING:
from spikingjelly.activation_based.ann2snn.converter import Converter
__all__ = ["RateCodingRecipe"]
def validate_rate_coding_mode(mode: Union[str, float]) -> None:
err_msg = "You have used a non-defined VoltageScale Method."
if isinstance(mode, str):
if not mode:
raise NotImplementedError(err_msg)
if mode[-1] == "%":
try:
percentile = float(mode[:-1])
if not (0.0 <= percentile <= 100.0):
raise NotImplementedError(err_msg)
except ValueError as exc:
raise NotImplementedError(err_msg) from exc
elif mode.lower() in ["max"]:
pass
else:
raise NotImplementedError(err_msg)
elif isinstance(mode, (int, float)) and not isinstance(mode, bool):
if not (0 < mode <= 1):
raise NotImplementedError(err_msg)
else:
raise NotImplementedError(err_msg)
class ChannelVoltageHook(nn.Module):
def __init__(
self,
mode: Union[str, float] = "Max",
momentum: float = 0.1,
channel_dim: int = 1,
eps: float = 1e-6,
) -> None:
super().__init__()
self.mode = mode
self.momentum = momentum
self.channel_dim = channel_dim
self.eps = eps
self.register_buffer("scale", torch.empty(0))
self.num_batches_tracked = 0
@staticmethod
def _normalize_channel_dim(x: torch.Tensor, channel_dim: int) -> int:
if x.dim() < 2:
raise ValueError(
"Channel-wise rate calibration requires activation tensors with "
"at least 2 dimensions."
)
if channel_dim < 0:
channel_dim += x.dim()
if channel_dim < 0 or channel_dim >= x.dim():
raise ValueError("channel_dim is out of range.")
return channel_dim
def forward(self, x: torch.Tensor) -> torch.Tensor:
err_msg = "You have used a non-defined VoltageScale Method."
channel_dim = self._normalize_channel_dim(x, self.channel_dim)
x_stat = torch.clamp(x.detach(), min=0)
if x_stat.dtype in [torch.float16, torch.bfloat16]:
x_stat = x_stat.to(torch.float32)
channel_values = x_stat.movedim(channel_dim, 0).reshape(
x_stat.shape[channel_dim], -1
)
if isinstance(self.mode, str):
if not self.mode:
raise NotImplementedError(err_msg)
if self.mode[-1] == "%":
try:
quantile = float(self.mode[:-1]) / 100.0
if not (0.0 <= quantile <= 1.0):
raise NotImplementedError(err_msg)
s_t = _safe_quantile(channel_values, quantile, dim=1)
except (ValueError, RuntimeError) as exc:
raise NotImplementedError(err_msg) from exc
elif self.mode.lower() in ["max"]:
s_t = channel_values.max(dim=1).values
else:
raise NotImplementedError(err_msg)
elif (
isinstance(self.mode, (int, float))
and not isinstance(self.mode, bool)
and 0 < self.mode <= 1
):
s_t = channel_values.max(dim=1).values * self.mode
else:
raise NotImplementedError(err_msg)
s_t = torch.clamp(s_t.to(device=x.device, dtype=x.dtype), min=self.eps)
if self.num_batches_tracked == 0:
self.scale = s_t.detach()
else:
self.scale = (
(1 - self.momentum) * self.scale.to(device=x.device, dtype=x.dtype)
+ self.momentum * s_t
).detach()
self.num_batches_tracked += x.shape[0]
return x
def compute_threshold(self) -> torch.Tensor:
if self.scale.numel() == 0:
raise ValueError("No calibration activations have been recorded.")
if not torch.isfinite(self.scale).all() or (self.scale <= 0).any():
raise ValueError("Channel-wise thresholds must be finite positive values.")
return self.scale.detach()
class ChannelVoltageHookFactory:
def __init__(
self,
mode: Union[str, float],
momentum: float,
channel_dim: int,
eps: float,
) -> None:
self.mode = mode
self.momentum = momentum
self.channel_dim = channel_dim
self.eps = eps
def create(self) -> ChannelVoltageHook:
return ChannelVoltageHook(
mode=self.mode,
momentum=self.momentum,
channel_dim=self.channel_dim,
eps=self.eps,
)
class ChannelWiseRateCodingReLURule:
def __init__(
self,
channel_dim: int = 1,
pre_spike_maxpool: bool = False,
half_threshold: bool = False,
) -> None:
self.channel_dim = channel_dim
self.pre_spike_maxpool = pre_spike_maxpool
self.half_threshold = half_threshold
self.relu_rule = ReLURule()
def match(self, node: fx.Node, modules: Dict[str, nn.Module]) -> bool:
return self.relu_rule.match(node, modules)
def insert_hooks(
self,
fx_model: fx.GraphModule,
node: fx.Node,
hook_factory: ChannelVoltageHookFactory,
hook_counts_per_prefix: Dict[str, int],
) -> fx.Node:
if not isinstance(node.target, str):
raise TypeError("node.target must be a module path string.")
parent, _, _ = node.target.rpartition(".")
key = parent or "__FIRST_LEVEL_OF_MODULE__"
counter = hook_counts_per_prefix.get(key, 0)
hook_counts_per_prefix[key] = counter + 1
target = (
f"{parent}.channel_voltage_hook_{counter}"
if parent
else f"channel_voltage_hook_{counter}"
)
modules = dict(fx_model.named_modules())
hook_input = node
users = list(node.users)
if self.pre_spike_maxpool and len(users) == 1:
user = users[0]
if (
user.op == "call_module"
and isinstance(user.target, str)
and isinstance(modules.get(user.target), nn.MaxPool2d)
):
hook_input = user
fx_model.add_submodule(target=target, m=hook_factory.create())
with fx_model.graph.inserting_after(n=hook_input):
hook_node = fx_model.graph.call_module(target, args=(hook_input,))
for user in list(hook_input.users):
if user is not hook_node:
user.replace_input_with(hook_input, hook_node)
return hook_node
def find_replacements(
self, fx_model: fx.GraphModule, modules: Dict[str, nn.Module]
) -> Iterator[Tuple[fx.Node, fx.Node]]:
for hook_node in fx_model.graph.nodes:
if hook_node.op != "call_module":
continue
if not isinstance(modules.get(hook_node.target), ChannelVoltageHook):
continue
if len(hook_node.args) == 0 or not isinstance(hook_node.args[0], fx.Node):
continue
hook_input_node = hook_node.args[0]
if hook_input_node.op != "call_module":
continue
if hook_input_node.target not in modules:
continue
if self.match(hook_input_node, modules):
yield hook_input_node, hook_node
continue
if not isinstance(modules.get(hook_input_node.target), nn.MaxPool2d):
continue
if len(hook_input_node.args) == 0 or not isinstance(
hook_input_node.args[0], fx.Node
):
continue
activation_node = hook_input_node.args[0]
if (
activation_node.op == "call_module"
and activation_node.target in modules
and self.match(activation_node, modules)
):
yield activation_node, hook_node
def replace_with_neurons(
self,
fx_model: fx.GraphModule,
activation_node: fx.Node,
hook_node: fx.Node,
neuron_factory: "NeuronFactory",
threshold_optimizer: "ThresholdOptimizer",
) -> None:
if len(activation_node.args) != 1:
raise ValueError(
f"The activation node {activation_node.target!r} must have exactly "
f"1 argument, but got {len(activation_node.args)}."
)
if not isinstance(hook_node.target, str):
raise TypeError("hook_node.target must be a module path string.")
hook = fx_model.get_submodule(hook_node.target)
if not isinstance(hook, ChannelVoltageHook):
raise TypeError("hook_node must target a ChannelVoltageHook module.")
if len(hook_node.args) != 1 or not isinstance(hook_node.args[0], fx.Node):
raise ValueError("hook_node must have exactly one FX node input.")
hook_input_node = hook_node.args[0]
pre_spike_maxpool = False
if hook_input_node is not activation_node:
if not (
hook_input_node.op == "call_module"
and isinstance(hook_input_node.target, str)
and isinstance(
fx_model.get_submodule(hook_input_node.target), nn.MaxPool2d
)
):
raise TypeError(
"Channel-wise RateCodingRecipe only supports hooks after "
"ReLU or after ReLU -> MaxPool2d."
)
pre_spike_maxpool = True
hook_input_node.replace_input_with(activation_node, activation_node.args[0])
threshold = hook.compute_threshold()
hook_parent, _, hook_leaf = hook_node.target.rpartition(".")
spike_leaf = hook_leaf.replace("channel_voltage_hook_", "channel_spiking_")
prefix = f"{hook_parent}.{spike_leaf}" if hook_parent else spike_leaf
target0 = f"{prefix}.scaler0"
target1 = f"{prefix}.if_node"
target2 = f"{prefix}.scaler1"
if self.half_threshold:
neuron_threshold = 1.0
m1 = neuron.HalfThresholdIFNode()
else:
m1 = neuron_factory.create(scale=1.0)
neuron_threshold = getattr(m1, "v_threshold", 1.0)
if hasattr(neuron_threshold, "item"):
if (
not hasattr(neuron_threshold, "numel")
or neuron_threshold.numel() == 1
):
neuron_threshold = neuron_threshold.item()
if not isinstance(neuron_threshold, (int, float)):
raise ValueError(
"Channel-wise RateCodingRecipe requires a scalar neuron threshold."
)
if not (neuron_threshold > 0.0) or not math.isfinite(neuron_threshold):
raise ValueError(
"Channel-wise RateCodingRecipe requires a finite positive "
f"neuron threshold, got {neuron_threshold}."
)
m0 = ChannelVoltageScaler(
neuron_threshold / threshold, channel_dim=self.channel_dim
)
m2 = ChannelVoltageScaler(threshold, channel_dim=self.channel_dim)
fx_model.add_submodule(target=target0, m=m0)
with fx_model.graph.inserting_after(n=hook_node):
spike_input_args = (
(hook_input_node,) if pre_spike_maxpool else activation_node.args
)
node0 = fx_model.graph.call_module(target0, args=spike_input_args)
fx_model.add_submodule(target=target1, m=m1)
with fx_model.graph.inserting_after(n=node0):
node1 = fx_model.graph.call_module(target1, args=(node0,))
fx_model.add_submodule(target=target2, m=m2)
with fx_model.graph.inserting_after(n=node1):
node2 = fx_model.graph.call_module(target2, args=(node1,))
hook_node.replace_all_uses_with(node2)
fx_model.graph.erase_node(hook_node)
if not pre_spike_maxpool:
activation_node.replace_all_uses_with(node2)
fx_model.graph.erase_node(activation_node)
[文档]
class RateCodingRecipe(ConversionRecipe):
def __init__(
self,
dataloader: Iterable,
mode: Union[str, float] = "Max",
momentum: float = 0.1,
fuse_flag: bool = True,
channel_wise: bool = False,
channel_dim: int = 1,
pre_spike_maxpool: bool = False,
half_threshold: bool = False,
eps: float = 1e-6,
rules: Optional[List[ActivationRule]] = None,
neuron_factory: Optional[NeuronFactory] = None,
threshold_optimizer: Optional[ThresholdOptimizer] = None,
) -> None:
r"""
**API Language** - :ref:`中文 <RateCodingRecipe.__init__-cn>` | :ref:`English <RateCodingRecipe.__init__-en>`
----
.. _RateCodingRecipe.__init__-cn:
* **中文**
构造传统 rate-coding ReLU→IFNode 转换 recipe。该 recipe 拥有
rate-coding 算法参数,并执行 Conv-BN 融合、VoltageHook 校准和
neuron replacement。
:param dataloader: 校准数据加载器。每个 batch 可为单输入 tensor、
``(input, target)`` 风格的 tuple/list,或包含 ``"input"`` /
``"image"`` / ``"images"`` 等输入键的 dict。默认 recipe 只支持
单输入校准;多输入模型应通过自定义 recipe 扩展。
:type dataloader: Iterable
:param mode: VoltageHook 统计模式,支持 ``"Max"``、百分位字符串和
``0 < mode <= 1`` 的浮点缩放。
:type mode: str or float
:param momentum: VoltageHook 动量。
:type momentum: float
:param fuse_flag: 是否执行 Conv-BN 融合。
:type fuse_flag: bool
:param channel_wise: 是否按通道统计 robust 激活尺度并使用 channel-wise
threshold。默认 ``False`` 以保持原有 layer-wise 行为。
:type channel_wise: bool
:param channel_dim: ``channel_wise=True`` 时的通道维。
:type channel_dim: int
:param pre_spike_maxpool: ``channel_wise=True`` 时,若匹配到
``ReLU -> MaxPool2d``,是否把 MaxPool2d 放到脉冲神经元之前。
:type pre_spike_maxpool: bool
:param half_threshold: ``channel_wise=True`` 时,是否使用半阈值膜电位初始化。
:type half_threshold: bool
:param eps: ``channel_wise=True`` 时的阈值数值下界。
:type eps: float
:param rules: 激活转换规则。默认 ``[ReLURule()]``。
:type rules: Optional[List[ActivationRule]]
:param neuron_factory: 脉冲神经元工厂。
:type neuron_factory: Optional[NeuronFactory]
:param threshold_optimizer: 阈值优化器。
:type threshold_optimizer: Optional[ThresholdOptimizer]
----
.. _RateCodingRecipe.__init__-en:
* **English**
Construct a traditional rate-coding ReLU-to-IFNode conversion recipe.
This recipe owns rate-coding algorithm parameters and performs Conv-BN
fusion, VoltageHook calibration and neuron replacement.
:param dataloader: Calibration dataloader. Each batch can be a
single-input tensor, a ``(input, target)``-style tuple/list, or a
dict with input-like keys such as ``"input"``, ``"image"``, or
``"images"``. The default recipe only supports single-input
calibration; multi-input models should extend a custom recipe.
:type dataloader: Iterable
:param mode: VoltageHook statistics mode. Supports ``"Max"``,
percentile strings, and float scaling with ``0 < mode <= 1``.
:type mode: str or float
:param momentum: VoltageHook momentum.
:type momentum: float
:param fuse_flag: Whether to fuse Conv-BN modules.
:type fuse_flag: bool
:param channel_wise: If ``True``, collect robust activation scales per
channel and use channel-wise thresholds. Defaults to ``False`` to
preserve the original layer-wise behaviour.
:type channel_wise: bool
:param channel_dim: Channel dimension used when ``channel_wise=True``.
:type channel_dim: int
:param pre_spike_maxpool: When ``channel_wise=True`` and a
``ReLU -> MaxPool2d`` pattern is matched, place MaxPool2d before the
spiking neuron.
:type pre_spike_maxpool: bool
:param half_threshold: When ``channel_wise=True``, initialize membrane
potential at half threshold.
:type half_threshold: bool
:param eps: Numeric lower bound for thresholds when ``channel_wise=True``.
:type eps: float
:param rules: Activation conversion rules. Defaults to ``[ReLURule()]``.
:type rules: Optional[List[ActivationRule]]
:param neuron_factory: Spiking-neuron factory.
:type neuron_factory: Optional[NeuronFactory]
:param threshold_optimizer: Threshold optimizer.
:type threshold_optimizer: Optional[ThresholdOptimizer]
"""
self.dataloader = dataloader
self.mode = mode
self.momentum = momentum
self.fuse_flag = fuse_flag
self.channel_wise = channel_wise
self.channel_dim = channel_dim
self.pre_spike_maxpool = pre_spike_maxpool
self.half_threshold = half_threshold
self.eps = eps
if channel_wise and rules is not None:
raise ValueError(
"RateCodingRecipe(channel_wise=True) does not support custom rules."
)
if channel_wise and threshold_optimizer is not None:
raise ValueError(
"RateCodingRecipe(channel_wise=True) does not support custom "
"threshold_optimizer."
)
if channel_wise and half_threshold and neuron_factory is not None:
raise ValueError(
"RateCodingRecipe(channel_wise=True, half_threshold=True) does not "
"support custom neuron_factory."
)
self.rules = (
[
ChannelWiseRateCodingReLURule(
channel_dim, pre_spike_maxpool, half_threshold
)
]
if channel_wise
else (rules if rules is not None else [ReLURule()])
)
self.neuron_factory = (
neuron_factory if neuron_factory is not None else NeuronFactory()
)
self.threshold_optimizer = (
threshold_optimizer
if threshold_optimizer is not None
else ThresholdOptimizer()
)
[文档]
def validate(self, converter: "Converter") -> None:
if self.dataloader is None:
raise ValueError(
"RateCodingRecipe requires a dataloader. "
"Pass dataloader to RateCodingRecipe."
)
self._check_mode()
if not isinstance(self.channel_wise, bool):
raise ValueError("channel_wise must be bool.")
if not isinstance(self.channel_dim, int):
raise ValueError("channel_dim must be int.")
if not isinstance(self.pre_spike_maxpool, bool):
raise ValueError("pre_spike_maxpool must be bool.")
if not isinstance(self.half_threshold, bool):
raise ValueError("half_threshold must be bool.")
if not (isinstance(self.eps, (int, float)) and self.eps > 0):
raise ValueError("eps must be a positive number.")
[文档]
def before_trace(self, converter: "Converter", ann: nn.Module) -> nn.Module:
ann.eval()
return ann
[文档]
def after_trace(
self, converter: "Converter", fx_model: fx.GraphModule
) -> fx.GraphModule:
return self._fuse(fx_model, fuse_flag=self.fuse_flag).to(converter.device)
[文档]
def insert_observers(
self, converter: "Converter", fx_model: fx.GraphModule
) -> fx.GraphModule:
return self._set_voltagehook(fx_model).to(converter.device)
[文档]
def calibrate(
self, converter: "Converter", fx_model: fx.GraphModule
) -> fx.GraphModule:
with torch.no_grad():
for _, data in enumerate(tqdm(self.dataloader)):
imgs = self._extract_batch_input(data)
if isinstance(imgs, torch.Tensor):
imgs = imgs.to(device=converter.device)
else:
if isinstance(imgs, (list, tuple, dict)):
raise ValueError(
"RateCodingRecipe supports single-input calibration "
"batches only. For multi-input models, subclass "
"ConversionRecipe or RateCodingRecipe and override "
"calibrate()."
)
imgs = torch.as_tensor(imgs, device=converter.device)
fx_model(imgs)
return fx_model
[文档]
def replace(
self, converter: "Converter", fx_model: fx.GraphModule
) -> fx.GraphModule:
r"""
**API Language** - :ref:`中文 <RateCodingRecipe.replace-cn>` | :ref:`English <RateCodingRecipe.replace-en>`
----
.. _RateCodingRecipe.replace-cn:
* **中文**
将已校准的 activation-hook 节点对替换为 rate-coding SNN 子图,并将
结果移动到当前转换 device。
:param converter: 执行当前 recipe 的转换器。
:type converter: Converter
:param fx_model: 已插入并校准 ``VoltageHook`` 的 ``GraphModule``。
:type fx_model: torch.fx.GraphModule
:return: 替换后的 ``GraphModule``。
:rtype: torch.fx.GraphModule
----
.. _RateCodingRecipe.replace-en:
* **English**
Replace calibrated activation-hook node pairs with rate-coding SNN
subgraphs, and move the result to the current conversion device.
:param converter: Converter that executes this recipe.
:type converter: Converter
:param fx_model: ``GraphModule`` with inserted and calibrated
``VoltageHook`` modules.
:type fx_model: torch.fx.GraphModule
:return: Replaced ``GraphModule``.
:rtype: torch.fx.GraphModule
"""
fx_model = self._replace_by_neurons(fx_model)
fx_model = adapt_step_mode_graph(
fx_model,
context="RateCodingRecipe step-mode backend",
wrap_module_types=_RATE_CODING_STATELESS_MODULE_TYPES,
safe_module_types=_RATE_CODING_SAFE_MODULE_TYPES,
safe_call_functions=(F.dropout,),
)
return fx_model.to(converter.device)
@staticmethod
def _extract_batch_input(data, _inside_input=False):
if isinstance(data, torch.Tensor):
return data
if isinstance(data, (list, tuple)):
if not data:
raise ValueError("Batch data is an empty list or tuple.")
if len(data) == 1:
return RateCodingRecipe._extract_batch_input(
data[0], _inside_input=_inside_input
)
if not _inside_input:
return RateCodingRecipe._extract_batch_input(
data[0], _inside_input=not isinstance(data[0], dict)
)
return data
if isinstance(data, dict):
if not data:
raise ValueError("Batch data is an empty dictionary.")
if _inside_input:
return data
for key in (
"input",
"image",
"images",
"img",
"x",
"data",
"pixel_values",
):
if key in data:
return RateCodingRecipe._extract_batch_input(
data[key], _inside_input=True
)
if len(data) != 1:
raise ValueError(
"Batch dictionaries with multiple fields must contain one "
"of 'input', 'image', 'images', 'img', 'x', 'data', or "
"'pixel_values'."
)
return RateCodingRecipe._extract_batch_input(
next(iter(data.values())), _inside_input=True
)
return data
def _check_mode(self):
validate_rate_coding_mode(self.mode)
@staticmethod
def _fuse(
fx_model: torch.fx.GraphModule, fuse_flag: bool = True
) -> torch.fx.GraphModule:
if not fuse_flag:
return fx_model
def matches_module_pattern(
pattern: Iterable[Type], node: fx.Node, modules: Dict[str, Any]
) -> bool:
if len(node.args) == 0:
return False
nodes: Tuple[Any, fx.Node] = (node.args[0], node)
for expected_type, current_node in zip(pattern, nodes):
if not isinstance(current_node, fx.Node):
return False
if current_node.op != "call_module":
return False
if not isinstance(current_node.target, str):
return False
if current_node.target not in modules:
return False
if not isinstance(modules[current_node.target], expected_type):
return False
return True
def replace_node_module(
node: fx.Node, modules: Dict[str, Any], new_module: torch.nn.Module
):
if not isinstance(node.target, str):
raise ValueError("FX module replacement requires a string target.")
parent_path, _, child_name = node.target.rpartition(".")
modules[node.target] = new_module
parent = fx_model.get_submodule(parent_path) if parent_path else fx_model
setattr(parent, child_name, new_module)
patterns = [
(nn.Conv1d, nn.BatchNorm1d),
(nn.Conv2d, nn.BatchNorm2d),
(nn.Conv3d, nn.BatchNorm3d),
]
modules = dict(fx_model.named_modules())
for pattern in patterns:
for node in list(fx_model.graph.nodes):
if matches_module_pattern(pattern, node, modules):
if len(node.args[0].users) > 1:
continue
conv = modules[node.args[0].target]
bn = modules[node.target]
fused_conv = fuse_conv_bn_eval(conv, bn)
replace_node_module(node.args[0], modules, fused_conv)
node.replace_all_uses_with(node.args[0])
fx_model.graph.erase_node(node)
fx_model.graph.lint()
fx_model.delete_all_unused_submodules()
fx_model.recompile()
return fx_model
def _set_voltagehook(self, fx_model: torch.fx.GraphModule) -> torch.fx.GraphModule:
if self.channel_wise:
hook_factory = ChannelVoltageHookFactory(
mode=self.mode,
momentum=self.momentum,
channel_dim=self.channel_dim,
eps=self.eps,
)
else:
hook_factory = HookFactory(mode=self.mode, momentum=self.momentum)
hook_counts_per_prefix: Dict[str, int] = {}
modules = dict(fx_model.named_modules())
for node in list(fx_model.graph.nodes):
if node.op != "call_module":
continue
if node.target not in modules:
continue
for rule in self.rules:
if rule.match(node, modules):
rule.insert_hooks(
fx_model, node, hook_factory, hook_counts_per_prefix
)
modules = dict(fx_model.named_modules())
break
fx_model.graph.lint()
fx_model.recompile()
return fx_model
def _replace_by_neurons(
self, fx_model: torch.fx.GraphModule
) -> torch.fx.GraphModule:
replaced_hooks = set()
replaced_activations = set()
for rule in self.rules:
modules = dict(fx_model.named_modules())
replacements = list(rule.find_replacements(fx_model, modules))
for activation_node, hook_node in replacements:
if (
hook_node in replaced_hooks
or activation_node in replaced_activations
):
continue
replaced_hooks.add(hook_node)
replaced_activations.add(activation_node)
rule.replace_with_neurons(
fx_model,
activation_node,
hook_node,
self.neuron_factory,
self.threshold_optimizer,
)
fx_model.graph.lint()
fx_model.delete_all_unused_submodules()
fx_model.recompile()
return fx_model