spikingjelly.activation_based.ann2snn.recipes.transformer_td_equivalent 源代码
from __future__ import annotations
import operator
from typing import Any, Dict, Optional, TYPE_CHECKING
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import fx
from spikingjelly.activation_based.ann2snn.operators import (
SNNMatrixOperator,
TDConv2d,
TDGELU,
TDLayerNorm,
TDLinear,
TDModule,
TDMultiheadAttention,
TDScaledDotProductAttention,
TDSoftmax,
)
from spikingjelly.activation_based.ann2snn.recipes.base import ConversionRecipe
from spikingjelly.activation_based.ann2snn.recipes.step_mode_adapters import (
_SHAPE_ONLY_MODULE_TYPES,
_TRANSFORMER_SAFE_MODULE_TYPES,
adapt_step_mode_graph,
)
if TYPE_CHECKING:
from spikingjelly.activation_based.ann2snn.converter import Converter
__all__ = ["TransformerTDEquivalentRecipe"]
def _td_softmax_dim(dim: int) -> int:
# `dim` indexes the ANN tensor. TD prepends a time axis, so non-negative
# dims shift by +1. Negative dims stay negative because TDSoftmax resolves
# them against the TD tensor rank, preserving the same feature axis.
return dim + 1 if dim >= 0 else dim
class _TDTanh(TDModule):
def ann_forward(self, x: torch.Tensor) -> torch.Tensor:
return torch.tanh(x)
def multi_step_forward(self, x_seq: torch.Tensor) -> torch.Tensor:
return self._td_sequence_forward((x_seq,), torch.tanh)
[文档]
class TransformerTDEquivalentRecipe(ConversionRecipe):
r"""
**API Language** - :ref:`中文 <TransformerTDEquivalentRecipe-cn>` | :ref:`English <TransformerTDEquivalentRecipe-en>`
----
.. _TransformerTDEquivalentRecipe-cn:
* **中文**
Transformer TD-equivalent operator 替换 recipe。该 recipe 不插入
observer,不运行 dataloader 校准,也不强制切换模型 train/eval 状态;它仅
将当前支持的 ANN core modules 和窄 attention 子集替换为 TD 等价算子。
----
.. _TransformerTDEquivalentRecipe-en:
* **English**
Transformer TD-equivalent operator replacement recipe. This recipe
does not insert observers, does not run dataloader calibration, and does not
force train/eval mode changes. It only replaces the currently supported ANN
core modules and narrow attention subset with TD-equivalent operators.
"""
def __init__(self, time_steps: Optional[int] = None) -> None:
self.time_steps = time_steps
[文档]
def validate(self, converter: "Converter") -> None:
if self.time_steps is None:
return
if (
not isinstance(self.time_steps, int)
or isinstance(self.time_steps, bool)
or self.time_steps <= 0
):
raise ValueError("time_steps must be a positive integer when set.")
[文档]
def replace(
self, converter: "Converter", fx_model: fx.GraphModule
) -> fx.GraphModule:
r"""
**API Language** - :ref:`中文 <TransformerTDEquivalentRecipe.replace-cn>` | :ref:`English <TransformerTDEquivalentRecipe.replace-en>`
----
.. _TransformerTDEquivalentRecipe.replace-cn:
* **中文**
将当前支持的 Transformer core modules、SDPA 调用和窄
``MultiheadAttention`` 调用替换为 TD-equivalent 算子。
该步骤不插入 observer,也不运行 rate-coding 校准。
:param converter: 执行当前 recipe 的转换器。
:type converter: Converter
:param fx_model: 已 trace 的 ``GraphModule``。
:type fx_model: torch.fx.GraphModule
:return: 替换后的 ``GraphModule``。
:rtype: torch.fx.GraphModule
:raises ValueError: 当 attention 调用或配置不在当前支持范围内时抛出。
----
.. _TransformerTDEquivalentRecipe.replace-en:
* **English**
Replace currently supported Transformer core modules, SDPA calls and
narrow ``MultiheadAttention`` calls with TD-equivalent
operators. This step does not insert observers or run rate-coding
calibration.
:param converter: Converter that executes this recipe.
:type converter: Converter
:param fx_model: Traced ``GraphModule``.
:type fx_model: torch.fx.GraphModule
:return: Replaced ``GraphModule``.
:rtype: torch.fx.GraphModule
:raises ValueError: If an attention call or configuration is outside
the currently supported subset.
"""
modules = dict(fx_model.named_modules())
for node in list(fx_model.graph.nodes):
if node.op != "call_module":
continue
if not isinstance(node.target, str) or node.target not in modules:
continue
module = modules[node.target]
replacement = self._make_td_operator(module, node)
if replacement is None:
continue
self._replace_submodule(fx_model, node.target, replacement)
modules[node.target] = replacement
self._replace_functional_td_ops(fx_model)
sdpa_index = 0
existing_modules = set(dict(fx_model.named_modules()).keys())
for node in list(fx_model.graph.nodes):
if (
node.op != "call_function"
or node.target is not F.scaled_dot_product_attention
):
continue
sdpa_kwargs = self._parse_sdpa_node(node)
target = f"td_scaled_dot_product_attention_{sdpa_index}"
while target in existing_modules:
sdpa_index += 1
target = f"td_scaled_dot_product_attention_{sdpa_index}"
sdpa_index += 1
fx_model.add_submodule(
target,
TDScaledDotProductAttention(
is_causal=sdpa_kwargs["is_causal"],
scale=sdpa_kwargs["scale"],
),
)
existing_modules.add(target)
with fx_model.graph.inserting_after(node):
new_node = fx_model.graph.call_module(
target,
args=(
sdpa_kwargs["query"],
sdpa_kwargs["key"],
sdpa_kwargs["value"],
sdpa_kwargs["attn_mask"],
),
)
node.replace_all_uses_with(new_node)
fx_model.graph.erase_node(node)
return adapt_step_mode_graph(
fx_model,
context="TransformerTDEquivalentRecipe step-mode backend",
wrap_module_types=_SHAPE_ONLY_MODULE_TYPES,
safe_module_types=_TRANSFORMER_SAFE_MODULE_TYPES,
safe_call_functions=(
torch.div,
operator.truediv,
),
)
[文档]
def finalize(self, converter: "Converter", fx_model: fx.GraphModule) -> nn.Module:
object.__setattr__(fx_model, "ann2snn_recipe", "transformer_td_equivalent")
if self.time_steps is not None:
object.__setattr__(fx_model, "time_steps", self.time_steps)
return fx_model
@staticmethod
def _replace_submodule(
fx_model: torch.fx.GraphModule, target: str, module: nn.Module
) -> None:
parent_name, _, child_name = target.rpartition(".")
parent = fx_model.get_submodule(parent_name) if parent_name else fx_model
setattr(parent, child_name, module)
@staticmethod
def _get_literal_argument(
node: fx.Node,
name: str,
position: int,
default: Any,
) -> Any:
if name in node.kwargs:
return node.kwargs[name]
if len(node.args) > position:
return node.args[position]
return default
@staticmethod
def _get_tensor_argument(node: fx.Node, name: str, position: int) -> Any:
if len(node.args) > position:
return node.args[position]
if name in node.kwargs:
return node.kwargs[name]
raise ValueError(
f"TD conversion got malformed {node.target!r} node: missing {name!r}."
)
@staticmethod
def _parse_sdpa_node(node: fx.Node) -> Dict[str, Any]:
dropout_p = TransformerTDEquivalentRecipe._get_literal_argument(
node, "dropout_p", 4, 0.0
)
if not isinstance(dropout_p, (int, float)) or float(dropout_p) != 0.0:
raise ValueError(
"TD SDPA conversion only supports literal dropout_p=0.0, "
f"but got {dropout_p!r}."
)
enable_gqa = TransformerTDEquivalentRecipe._get_literal_argument(
node, "enable_gqa", 7, False
)
if enable_gqa is not False:
raise ValueError("TD SDPA conversion does not support enable_gqa=True.")
is_causal = TransformerTDEquivalentRecipe._get_literal_argument(
node, "is_causal", 5, False
)
if not isinstance(is_causal, bool):
raise ValueError(
"TD SDPA conversion only supports literal bool is_causal, "
f"but got {is_causal!r}."
)
scale = TransformerTDEquivalentRecipe._get_literal_argument(
node, "scale", 6, None
)
if scale is not None and not isinstance(scale, (int, float)):
raise ValueError(
"TD SDPA conversion only supports literal numeric scale or None, "
f"but got {scale!r}."
)
return {
"query": TransformerTDEquivalentRecipe._get_tensor_argument(
node, "query", 0
),
"key": TransformerTDEquivalentRecipe._get_tensor_argument(
node, "key", 1
),
"value": TransformerTDEquivalentRecipe._get_tensor_argument(
node, "value", 2
),
"attn_mask": TransformerTDEquivalentRecipe._get_literal_argument(
node, "attn_mask", 3, None
),
"is_causal": is_causal,
"scale": None if scale is None else float(scale),
}
@staticmethod
def _check_mha_node(module: nn.MultiheadAttention, node: fx.Node) -> None:
if module.dropout != 0.0:
raise ValueError("TD MHA conversion only supports dropout=0.0.")
if not module.batch_first:
raise ValueError("TD MHA conversion only supports batch_first=True.")
if module.kdim != module.embed_dim or module.vdim != module.embed_dim:
raise ValueError(
"TD MHA conversion only supports kdim == vdim == embed_dim."
)
if module.in_proj_weight is None:
raise ValueError("TD MHA conversion requires packed in_proj_weight.")
if module.bias_k is not None or module.bias_v is not None:
raise ValueError("TD MHA conversion does not support add_bias_kv.")
if module.add_zero_attn:
raise ValueError("TD MHA conversion does not support add_zero_attn.")
need_weights = TransformerTDEquivalentRecipe._get_literal_argument(
node, "need_weights", 4, True
)
if need_weights is not False:
raise ValueError("TD MHA conversion requires need_weights=False.")
key_padding_mask = TransformerTDEquivalentRecipe._get_literal_argument(
node, "key_padding_mask", 3, None
)
if key_padding_mask is not None:
raise ValueError("TD MHA conversion does not support key_padding_mask.")
average_attn_weights = TransformerTDEquivalentRecipe._get_literal_argument(
node, "average_attn_weights", 6, True
)
if average_attn_weights is not True:
raise ValueError(
"TD MHA conversion does not support average_attn_weights=False."
)
@staticmethod
def _copy_mha_parameters(
source: nn.MultiheadAttention,
target: TDMultiheadAttention,
) -> None:
if source.in_proj_weight is None:
raise ValueError("TD MHA conversion requires packed in_proj_weight.")
with torch.no_grad():
q_weight, k_weight, v_weight = source.in_proj_weight.chunk(3, dim=0)
target.q_proj.weight.copy_(q_weight)
target.k_proj.weight.copy_(k_weight)
target.v_proj.weight.copy_(v_weight)
if source.in_proj_bias is not None:
q_bias, k_bias, v_bias = source.in_proj_bias.chunk(3, dim=0)
if target.q_proj.bias is not None:
target.q_proj.bias.copy_(q_bias)
if target.k_proj.bias is not None:
target.k_proj.bias.copy_(k_bias)
if target.v_proj.bias is not None:
target.v_proj.bias.copy_(v_bias)
target.out_proj.weight.copy_(source.out_proj.weight)
if source.out_proj.bias is not None and target.out_proj.bias is not None:
target.out_proj.bias.copy_(source.out_proj.bias)
def _make_td_operator(
self,
module: nn.Module,
node: Optional[fx.Node] = None,
) -> Optional[nn.Module]:
if isinstance(module, TDModule):
return None
if isinstance(module, nn.Linear):
td_module = TDLinear(
module.in_features,
module.out_features,
bias=module.bias is not None,
device=module.weight.device,
dtype=module.weight.dtype,
)
with torch.no_grad():
td_module.weight.copy_(module.weight)
if module.bias is not None:
td_module.bias.copy_(module.bias)
td_module.weight.requires_grad = module.weight.requires_grad
if module.bias is not None:
td_module.bias.requires_grad = module.bias.requires_grad
td_module.train(module.training)
return td_module
if isinstance(module, nn.Conv2d):
td_module = TDConv2d(
module.in_channels,
module.out_channels,
module.kernel_size,
stride=module.stride,
padding=module.padding,
dilation=module.dilation,
groups=module.groups,
bias=module.bias is not None,
padding_mode=module.padding_mode,
device=module.weight.device,
dtype=module.weight.dtype,
)
with torch.no_grad():
td_module.weight.copy_(module.weight)
if module.bias is not None:
td_module.bias.copy_(module.bias)
td_module.weight.requires_grad = module.weight.requires_grad
if module.bias is not None:
td_module.bias.requires_grad = module.bias.requires_grad
td_module.train(module.training)
return td_module
if isinstance(module, nn.LayerNorm):
td_module = TDLayerNorm(
module.normalized_shape,
eps=module.eps,
elementwise_affine=module.elementwise_affine,
bias=module.bias is not None,
device=(module.weight.device if module.weight is not None else None),
dtype=(module.weight.dtype if module.weight is not None else None),
)
with torch.no_grad():
if module.weight is not None:
td_module.weight.copy_(module.weight)
if module.bias is not None:
td_module.bias.copy_(module.bias)
td_module.train(module.training)
return td_module
if isinstance(module, nn.GELU):
td_module = TDGELU(approximate=getattr(module, "approximate", "none"))
td_module.train(module.training)
return td_module
if isinstance(module, nn.Tanh):
td_module = _TDTanh()
td_module.train(module.training)
return td_module
if isinstance(module, nn.Softmax):
dim = module.dim
if not isinstance(dim, int):
raise ValueError(
"TD softmax conversion requires nn.Softmax dim to be a literal int."
)
return TDSoftmax(dim=_td_softmax_dim(dim))
if isinstance(module, nn.MultiheadAttention):
if node is None:
raise ValueError("TD MHA conversion requires an FX node.")
self._check_mha_node(module, node)
td_module = TDMultiheadAttention(
module.embed_dim,
module.num_heads,
dropout=module.dropout,
bias=module.in_proj_bias is not None,
batch_first=module.batch_first,
device=module.in_proj_weight.device,
dtype=module.in_proj_weight.dtype,
)
self._copy_mha_parameters(module, td_module)
td_module.train(module.training)
return td_module
return None
@staticmethod
def _insert_call_module_after(
fx_model: fx.GraphModule,
node: fx.Node,
module: nn.Module,
prefix: str,
index: int,
args: tuple[Any, ...],
) -> int:
existing = set(dict(fx_model.named_modules()).keys())
target = f"{prefix}_{index}"
while target in existing:
index += 1
target = f"{prefix}_{index}"
fx_model.add_submodule(target, module)
with fx_model.graph.inserting_after(node):
new_node = fx_model.graph.call_module(target, args=args)
node.replace_all_uses_with(new_node)
fx_model.graph.erase_node(node)
return index + 1
def _replace_functional_td_ops(self, fx_model: fx.GraphModule) -> None:
softmax_index = 0
matmul_index = 0
gelu_index = 0
tanh_index = 0
for node in list(fx_model.graph.nodes):
if node.op == "call_function" and node.target is F.gelu:
approximate = self._get_literal_argument(node, "approximate", 1, "none")
if approximate not in ("none", "tanh"):
raise ValueError(
"TD GELU conversion requires approximate to be 'none' or 'tanh'."
)
gelu_index = self._insert_call_module_after(
fx_model,
node,
TDGELU(approximate=approximate),
"td_gelu",
gelu_index,
(self._get_tensor_argument(node, "input", 0),),
)
continue
if node.op == "call_function" and node.target in (torch.tanh, F.tanh):
tanh_index = self._insert_call_module_after(
fx_model,
node,
_TDTanh(),
"td_tanh",
tanh_index,
(self._get_tensor_argument(node, "input", 0),),
)
continue
if node.op == "call_function" and node.target in (F.softmax, torch.softmax):
dim = self._get_literal_argument(node, "dim", 1, None)
if not isinstance(dim, int):
raise ValueError("TD softmax conversion requires literal int dim.")
softmax_index = self._insert_call_module_after(
fx_model,
node,
TDSoftmax(dim=_td_softmax_dim(dim)),
"td_softmax",
softmax_index,
(self._get_tensor_argument(node, "input", 0),),
)
continue
if node.op == "call_method" and node.target == "softmax":
dim = self._get_literal_argument(node, "dim", 1, None)
if not isinstance(dim, int):
raise ValueError(
"TD tensor.softmax conversion requires literal int dim."
)
softmax_index = self._insert_call_module_after(
fx_model,
node,
TDSoftmax(dim=_td_softmax_dim(dim)),
"td_softmax",
softmax_index,
(node.args[0],),
)
continue
if node.op == "call_function" and node.target in (
torch.matmul,
operator.matmul,
):
matmul_index = self._insert_call_module_after(
fx_model,
node,
SNNMatrixOperator(),
"td_matmul",
matmul_index,
(
self._get_tensor_argument(node, "input", 0),
self._get_tensor_argument(node, "other", 1),
),
)
continue
if node.op == "call_method" and node.target == "matmul":
if len(node.args) < 2:
raise ValueError(
"TD tensor.matmul conversion got malformed matmul node."
)
matmul_index = self._insert_call_module_after(
fx_model,
node,
SNNMatrixOperator(),
"td_matmul",
matmul_index,
(node.args[0], node.args[1]),
)
continue
fx_model.graph.lint()
fx_model.delete_all_unused_submodules()
fx_model.recompile()