spikingjelly.activation_based.ann2snn.recipes.spikezip_qann 源代码

from __future__ import annotations

import copy
import math
import types
from typing import TYPE_CHECKING, Optional

import torch
import torch.nn as nn

from spikingjelly.activation_based import base
from spikingjelly.activation_based.ann2snn.operators import (
    TDConv2d,
    TDLayerNorm,
    TDLinear,
    TDSoftmax,
)
from spikingjelly.activation_based.ann2snn.recipes.base import ModuleConversionRecipe
from spikingjelly.activation_based.neuron import STBIFNeuron

if TYPE_CHECKING:
    from spikingjelly.activation_based.ann2snn.converter import ModuleConverter


__all__ = [
    "STBIFNeuron",
    "SpikeZIPConv2d",
    "SpikeZIPEmbedding",
    "SpikeZIPLayerNorm",
    "SpikeZIPLinear",
    "SpikeZIPRobertaSelfAttention",
    "SpikeZIPSoftmax",
    "SpikeZIPTFQANNRecipe",
    "SpikeZIPViTSelfAttention",
]


class SpikeZIPLinear(TDLinear):
    def __init__(
        self,
        linear: nn.Linear,
        level: int,
        bias_steps: Optional[int] = None,
        step_mode: str = "s",
    ) -> None:
        super().__init__(
            linear.in_features,
            linear.out_features,
            bias=False,
            device=linear.weight.device,
            dtype=linear.weight.dtype,
            step_mode=step_mode,
        )
        with torch.no_grad():
            self.weight.copy_(linear.weight)
        if linear.bias is None:
            self.register_parameter("spikezip_bias", None)
        else:
            self.spikezip_bias = nn.Parameter(linear.bias.detach().clone())
        self.level = int(level)
        self.bias_steps = self.level if bias_steps is None else int(bias_steps)
        if self.bias_steps <= 0:
            raise ValueError("bias_steps must be positive.")
        self.reset()

    def reset(self) -> None:
        super().reset()
        self.realize_time = self.bias_steps
        self.is_work = False

    def _release_bias(self, y: torch.Tensor) -> torch.Tensor:
        if self.spikezip_bias is not None:
            if self.realize_time > 0:
                self.realize_time -= 1
                self.is_work = True
                bias = self.spikezip_bias.to(device=y.device, dtype=y.dtype)
                y = y + bias / self.bias_steps
        return y

    def single_step_forward(self, x: torch.Tensor) -> torch.Tensor:
        y = super().single_step_forward(x)
        self.is_work = not bool((x == 0).all())
        return self._release_bias(y)

    def multi_step_forward(self, x_seq: torch.Tensor) -> torch.Tensor:
        y_seq = super().multi_step_forward(x_seq)
        active = bool((x_seq != 0).any())
        bias_steps = min(x_seq.shape[0], self.realize_time)
        if self.spikezip_bias is not None and bias_steps > 0:
            bias = self.spikezip_bias.to(device=x_seq.device, dtype=x_seq.dtype)
            view_shape = (1,) * (y_seq.dim() - 1) + (bias.numel(),)
            y_seq[:bias_steps] = (
                y_seq[:bias_steps] + bias.view(view_shape) / self.bias_steps
            )
            self.realize_time -= bias_steps
        self.is_work = active or bias_steps > 0
        return y_seq


class SpikeZIPConv2d(TDConv2d):
    def __init__(
        self,
        conv: nn.Conv2d,
        level: int,
        bias_steps: Optional[int] = None,
        step_mode: str = "s",
    ) -> None:
        super().__init__(
            conv.in_channels,
            conv.out_channels,
            conv.kernel_size,
            stride=conv.stride,
            padding=conv.padding,
            dilation=conv.dilation,
            groups=conv.groups,
            bias=False,
            padding_mode=conv.padding_mode,
            device=conv.weight.device,
            dtype=conv.weight.dtype,
            step_mode=step_mode,
        )
        with torch.no_grad():
            self.weight.copy_(conv.weight)
        if conv.bias is None:
            self.register_parameter("spikezip_bias", None)
        else:
            self.spikezip_bias = nn.Parameter(conv.bias.detach().clone())
        self.level = int(level)
        self.bias_steps = self.level if bias_steps is None else int(bias_steps)
        if self.bias_steps <= 0:
            raise ValueError("bias_steps must be positive.")
        self.reset()

    def reset(self) -> None:
        super().reset()
        self.realize_time = self.bias_steps
        self.is_work = False

    def _release_bias(self, y: torch.Tensor) -> torch.Tensor:
        if self.spikezip_bias is not None:
            if self.realize_time > 0:
                self.realize_time -= 1
                self.is_work = True
                bias = self.spikezip_bias.to(device=y.device, dtype=y.dtype)
                y = y + bias.view(1, -1, 1, 1) / self.bias_steps
        return y

    def single_step_forward(self, x: torch.Tensor) -> torch.Tensor:
        y = super().single_step_forward(x)
        self.is_work = not bool((x == 0).all())
        return self._release_bias(y)

    def multi_step_forward(self, x_seq: torch.Tensor) -> torch.Tensor:
        y_seq = super().multi_step_forward(x_seq)
        active = bool((x_seq != 0).any())
        bias_steps = min(x_seq.shape[0], self.realize_time)
        if self.spikezip_bias is not None and bias_steps > 0:
            bias = self.spikezip_bias.to(device=x_seq.device, dtype=x_seq.dtype)
            y_seq[:bias_steps] = (
                y_seq[:bias_steps] + bias.view(1, 1, -1, 1, 1) / self.bias_steps
            )
            self.realize_time -= bias_steps
        self.is_work = active or bias_steps > 0
        return y_seq


class SpikeZIPEmbedding(base.MemoryModule):
    def __init__(self, embedding: nn.Embedding) -> None:
        super().__init__()
        self.embedding = embedding
        self.step_mode = "s"
        self.reset()

    def reset(self) -> None:
        self.t = 0

    def single_step_forward(self, x: torch.Tensor) -> torch.Tensor:
        if self.t == 0:
            y = self.embedding(x)
            self.t += 1
            return y
        return torch.zeros(
            *x.shape,
            self.embedding.embedding_dim,
            device=x.device,
            dtype=self.embedding.weight.dtype,
        )

    def multi_step_forward(self, x_seq: torch.Tensor) -> torch.Tensor:
        if x_seq.shape[0] == 0:
            raise ValueError("SpikeZIPEmbedding expects a non-empty sequence.")
        y0 = self.embedding(x_seq[0])
        y_seq = torch.zeros(
            x_seq.shape[0],
            *y0.shape,
            device=y0.device,
            dtype=y0.dtype,
        )
        y_seq[0] = y0
        self.t = x_seq.shape[0]
        return y_seq


class SpikeZIPLayerNorm(TDLayerNorm):
    def __init__(self, source: nn.LayerNorm) -> None:
        super().__init__(
            source.normalized_shape,
            eps=source.eps,
            elementwise_affine=source.elementwise_affine,
            bias=source.bias is not None,
            device=(source.weight.device if source.weight is not None else None),
            dtype=(source.weight.dtype if source.weight is not None else None),
            step_mode="s",
        )
        self.load_state_dict(source.state_dict())


class SpikeZIPSoftmax(TDSoftmax):
    def __init__(self, dim: int = -1) -> None:
        super().__init__(dim=dim, step_mode="s")


def _spikezip_matmul_delta(a_t, b_t, a_sum, b_sum, transpose_b: bool = False):
    b_t_arg = b_t.transpose(-2, -1) if transpose_b else b_t
    b_sum_arg = b_sum.transpose(-2, -1) if transpose_b else b_sum
    return a_sum @ b_t_arg + a_t @ b_sum_arg - a_t @ b_t_arg


def _temporal_difference(y_cum: torch.Tensor) -> torch.Tensor:
    y_seq = torch.empty_like(y_cum)
    y_seq[0] = y_cum[0]
    y_seq[1:] = y_cum[1:] - y_cum[:-1]
    return y_seq


def _spikezip_matmul_sequence_delta(
    a_seq: torch.Tensor,
    b_seq: torch.Tensor,
    transpose_b: bool = False,
) -> torch.Tensor:
    a_cum = a_seq.cumsum(dim=0)
    b_cum = b_seq.cumsum(dim=0)
    if transpose_b:
        b_cum = b_cum.transpose(-2, -1)
    return _temporal_difference(a_cum @ b_cum)


def _step_modes(module: nn.Module) -> dict[nn.Module, str]:
    return {
        child: child.step_mode
        for child in module.modules()
        if child is not module and hasattr(child, "step_mode")
    }


def _restore_step_modes(step_modes: dict[nn.Module, str]) -> None:
    for module, step_mode in step_modes.items():
        module.step_mode = step_mode


class SpikeZIPRobertaSelfAttention(base.MemoryModule):
    def __init__(self, source: nn.Module, level: int) -> None:
        super().__init__()
        self.num_attention_heads = int(source.num_attention_heads)
        self.attention_head_size = int(source.attention_head_size)
        self.all_head_size = int(
            getattr(
                source,
                "all_head_size",
                self.num_attention_heads * self.attention_head_size,
            )
        )
        self.query = SpikeZIPLinear(source.query, level)
        self.key = SpikeZIPLinear(source.key, level)
        self.value = SpikeZIPLinear(source.value, level)
        self.query_if = STBIFNeuron.from_quantizer(source.query_quan)
        self.key_if = STBIFNeuron.from_quantizer(source.key_quan)
        self.value_if = STBIFNeuron.from_quantizer(source.value_quan)
        self.attn_if = STBIFNeuron.from_quantizer(source.attn_quan)
        self.after_attn_if = STBIFNeuron.from_quantizer(source.after_attn_quan)
        self.softmax = SpikeZIPSoftmax(dim=-1)
        self.dropout = copy.deepcopy(getattr(source, "dropout", nn.Identity()))
        self.step_mode = "s"
        self.t = 0
        self.position_embedding_type = getattr(
            source,
            "position_embedding_type",
            "absolute",
        )
        self.is_decoder = bool(getattr(source, "is_decoder", False))
        if self.position_embedding_type != "absolute":
            raise ValueError(
                "SpikeZIPTFQANNRecipe v1 supports absolute position attention only."
            )

    def reset(self) -> None:
        for module in (
            self.query,
            self.key,
            self.value,
            self.query_if,
            self.key_if,
            self.value_if,
            self.attn_if,
            self.after_attn_if,
            self.softmax,
        ):
            module.reset()
        self.t = 0

    def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor:
        shape = (*x.size()[:-1], self.num_attention_heads, self.attention_head_size)
        return x.view(shape).permute(0, 2, 1, 3)

    def transpose_sequence_for_scores(self, x_seq: torch.Tensor) -> torch.Tensor:
        shape = (
            *x_seq.size()[:-1],
            self.num_attention_heads,
            self.attention_head_size,
        )
        return x_seq.view(shape).permute(0, 1, 3, 2, 4)

    def single_step_forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        encoder_hidden_states: Optional[torch.Tensor] = None,
        encoder_attention_mask: Optional[torch.Tensor] = None,
        past_key_value=None,
        output_attentions: bool = False,
    ):
        if encoder_hidden_states is not None or encoder_attention_mask is not None:
            raise ValueError(
                "SpikeZIPTFQANNRecipe v1 does not support cross-attention."
            )
        if past_key_value is not None or self.is_decoder:
            raise ValueError("SpikeZIPTFQANNRecipe v1 does not support decoder cache.")
        if head_mask is not None:
            raise ValueError("SpikeZIPTFQANNRecipe v1 does not support head_mask.")
        query_layer = self.transpose_for_scores(
            self.query_if(self.query(hidden_states))
        )
        key_layer = self.transpose_for_scores(self.key_if(self.key(hidden_states)))
        value_layer = self.transpose_for_scores(
            self.value_if(self.value(hidden_states))
        )

        q_sum = self.transpose_for_scores(self.query_if.accumulated)
        k_sum = self.transpose_for_scores(self.key_if.accumulated)
        scores = _spikezip_matmul_delta(query_layer, key_layer, q_sum, k_sum, True)
        scores = scores / math.sqrt(self.attention_head_size)
        if attention_mask is not None and self.t == 0:
            scores = scores + attention_mask
        attention_probs = self.softmax(scores)
        attention_probs = self.dropout(attention_probs)
        attention_probs = self.attn_if(attention_probs)
        context = _spikezip_matmul_delta(
            attention_probs,
            value_layer,
            self.attn_if.accumulated,
            self.transpose_for_scores(self.value_if.accumulated),
        )
        context = self.after_attn_if(context)
        context = context.permute(0, 2, 1, 3).contiguous()
        context = context.view(*context.size()[:-2], self.all_head_size)
        self.t += 1
        return (context, attention_probs) if output_attentions else (context,)

    def multi_step_forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        encoder_hidden_states: Optional[torch.Tensor] = None,
        encoder_attention_mask: Optional[torch.Tensor] = None,
        past_key_value=None,
        output_attentions: bool = False,
    ):
        if encoder_hidden_states is not None or encoder_attention_mask is not None:
            raise ValueError(
                "SpikeZIPTFQANNRecipe v1 does not support cross-attention."
            )
        if past_key_value is not None or self.is_decoder:
            raise ValueError("SpikeZIPTFQANNRecipe v1 does not support decoder cache.")
        if head_mask is not None:
            raise ValueError("SpikeZIPTFQANNRecipe v1 does not support head_mask.")
        previous_step_modes = _step_modes(self)
        try:
            for module in previous_step_modes:
                module.step_mode = "m"
            query_layer = self.transpose_sequence_for_scores(
                self.query_if(self.query(hidden_states))
            )
            key_layer = self.transpose_sequence_for_scores(
                self.key_if(self.key(hidden_states))
            )
            value_layer = self.transpose_sequence_for_scores(
                self.value_if(self.value(hidden_states))
            )
            scores = _spikezip_matmul_sequence_delta(
                query_layer,
                key_layer,
                transpose_b=True,
            )
            scores = scores / math.sqrt(self.attention_head_size)
            if attention_mask is not None:
                scores = scores.clone()
                scores[0] = scores[0] + attention_mask
            attention_probs = self.softmax(scores)
            attention_probs = self.dropout(attention_probs)
            attention_probs = self.attn_if(attention_probs)
            context_seq = _spikezip_matmul_sequence_delta(
                attention_probs,
                value_layer,
            )
            context_seq = self.after_attn_if(context_seq)
            context_seq = context_seq.permute(0, 1, 3, 2, 4).contiguous()
            context_seq = context_seq.view(
                *context_seq.size()[:-2],
                self.all_head_size,
            )
            if output_attentions:
                return context_seq, attention_probs
            return (context_seq,)
        finally:
            _restore_step_modes(previous_step_modes)


class SpikeZIPViTSelfAttention(base.MemoryModule):
    def __init__(self, source: nn.Module, level: int) -> None:
        super().__init__()
        self.num_heads = int(source.num_heads)
        self.head_dim = int(source.head_dim)
        self.scale = float(getattr(source, "scale", self.head_dim**-0.5))
        self.is_softmax = bool(getattr(source, "is_softmax", True))
        self.qkv = SpikeZIPLinear(source.qkv, level, bias_steps=1)
        self.proj = SpikeZIPLinear(source.proj, level, bias_steps=1)
        self.q_if = STBIFNeuron.from_quantizer(source.quan_q)
        self.k_if = STBIFNeuron.from_quantizer(source.quan_k)
        self.v_if = STBIFNeuron.from_quantizer(source.quan_v)
        self.attn_if = STBIFNeuron.from_quantizer(source.attn_quan)
        self.after_attn_if = STBIFNeuron.from_quantizer(source.after_attn_quan)
        self.proj_if = STBIFNeuron.from_quantizer(source.quan_proj)
        self.attn_drop = copy.deepcopy(getattr(source, "attn_drop", nn.Identity()))
        self.proj_drop = copy.deepcopy(getattr(source, "proj_drop", nn.Identity()))
        self.step_mode = "s"
        if self.is_softmax:
            self.softmax = SpikeZIPSoftmax(dim=-1)

    def reset(self) -> None:
        modules = [
            self.qkv,
            self.proj,
            self.q_if,
            self.k_if,
            self.v_if,
            self.attn_if,
            self.after_attn_if,
            self.proj_if,
        ]
        if self.is_softmax:
            modules.append(self.softmax)
        for module in modules:
            module.reset()

    def single_step_forward(self, x: torch.Tensor) -> torch.Tensor:
        batch_size, seq_len, channels = x.shape
        qkv = self.qkv(x).reshape(
            batch_size,
            seq_len,
            3,
            self.num_heads,
            self.head_dim,
        )
        qkv = qkv.permute(2, 0, 3, 1, 4)
        query, key, value = qkv.unbind(0)

        query = self.q_if(query)
        key = self.k_if(key)
        value = self.v_if(value)

        query = query * self.scale
        q_sum = self.q_if.accumulated * self.scale
        k_sum = self.k_if.accumulated
        attention = _spikezip_matmul_delta(query, key, q_sum, k_sum, True)

        if self.is_softmax:
            attention = self.softmax(attention)
            attention = self.attn_if(attention)
            attention_sum = self.attn_if.accumulated
        else:
            attention = self.attn_if(attention) / seq_len
            attention_sum = self.attn_if.accumulated / seq_len

        attention = self.attn_drop(attention)
        context = _spikezip_matmul_delta(
            attention,
            value,
            attention_sum,
            self.v_if.accumulated,
        )
        context = self.after_attn_if(context)
        context = context.transpose(1, 2).reshape(batch_size, seq_len, channels)
        context = self.proj(context)
        context = self.proj_drop(context)
        return self.proj_if(context)

    def multi_step_forward(self, x_seq: torch.Tensor) -> torch.Tensor:
        previous_step_modes = _step_modes(self)
        try:
            for module in previous_step_modes:
                module.step_mode = "m"
            batch_size, seq_len, channels = x_seq.shape[1:]
            qkv = self.qkv(x_seq).reshape(
                x_seq.shape[0],
                batch_size,
                seq_len,
                3,
                self.num_heads,
                self.head_dim,
            )
            qkv = qkv.permute(3, 0, 1, 4, 2, 5)
            query, key, value = qkv.unbind(0)

            query = self.q_if(query)
            key = self.k_if(key)
            value = self.v_if(value)

            attention = _spikezip_matmul_sequence_delta(
                query * self.scale,
                key,
                transpose_b=True,
            )
            if self.is_softmax:
                attention = self.softmax(attention)
                attention = self.attn_if(attention)
                attention_for_context = attention
            else:
                attention = self.attn_if(attention) / seq_len
                attention_for_context = attention

            attention_for_context = self.attn_drop(attention_for_context)
            context = _spikezip_matmul_sequence_delta(
                attention_for_context,
                value,
            )
            context = self.after_attn_if(context)
            context = context.transpose(2, 3).reshape(
                x_seq.shape[0],
                batch_size,
                seq_len,
                channels,
            )
            context = self.proj(context)
            context = self.proj_drop(context)
            return self.proj_if(context)
        finally:
            _restore_step_modes(previous_step_modes)


def _is_roberta_qattention(module: nn.Module) -> bool:
    return all(
        hasattr(module, name)
        for name in (
            "query",
            "query_quan",
            "key",
            "key_quan",
            "value",
            "value_quan",
            "attn_quan",
            "after_attn_quan",
            "num_attention_heads",
            "attention_head_size",
            "all_head_size",
            "dropout",
        )
    )


def _is_vit_qattention(module: nn.Module) -> bool:
    return all(
        hasattr(module, name)
        for name in (
            "qkv",
            "quan_q",
            "quan_k",
            "quan_v",
            "attn_quan",
            "after_attn_quan",
            "proj",
            "quan_proj",
            "num_heads",
            "head_dim",
            "scale",
            "attn_drop",
            "proj_drop",
        )
    )


def _spikezip_vit_patch_embed(model: nn.Module, x: torch.Tensor) -> torch.Tensor:
    patch_embed = model.patch_embed
    used_proj = False
    input_dim = x.dim()
    if hasattr(patch_embed, "proj"):
        x = patch_embed.proj(x)
        used_proj = True
    else:
        x = patch_embed(x)
        if x.dim() == input_dim - 1:
            return x
    if x.dim() == 4:
        x = x.flatten(2).transpose(1, 2)
    elif x.dim() == 5:
        x = x.flatten(3).transpose(2, 3)
    else:
        raise ValueError("SpikeZIP ViT patch_embed expects [B,C,H,W] or [T,B,C,H,W].")
    if used_proj and hasattr(patch_embed, "norm"):
        x = patch_embed.norm(x)
    return x


def _spikezip_vit_token_sequence(
    model: nn.Module,
    time_steps: int,
    batch_size: int,
) -> tuple[torch.Tensor, torch.Tensor]:
    cls = model.cls_token.expand(batch_size, -1, -1)
    cls_seq = torch.zeros(
        time_steps,
        *cls.shape,
        device=cls.device,
        dtype=cls.dtype,
    )
    pos_seq = torch.zeros(
        time_steps,
        *model.pos_embed.shape,
        device=model.pos_embed.device,
        dtype=model.pos_embed.dtype,
    )
    cls_seq[0] = cls
    pos_seq[0] = model.pos_embed
    return cls_seq, pos_seq


def _spikezip_vit_single_tokens(
    model: nn.Module,
    batch_size: int,
) -> tuple[torch.Tensor, torch.Tensor]:
    t = getattr(model, "_spikezip_t", 0)
    if t == 0:
        cls = model.cls_token.expand(batch_size, -1, -1)
        pos = model.pos_embed
    else:
        cls = torch.zeros(
            batch_size,
            *model.cls_token.shape[1:],
            device=model.cls_token.device,
            dtype=model.cls_token.dtype,
        )
        pos = torch.zeros_like(model.pos_embed)
    model._spikezip_t = t + 1
    return cls, pos


def _spikezip_vit_forward_features(self: nn.Module, x: torch.Tensor) -> torch.Tensor:
    if x.dim() == 4:
        hidden = _spikezip_vit_patch_embed(self, x)
        cls, pos = _spikezip_vit_single_tokens(self, hidden.shape[0])
        hidden = torch.cat((cls, hidden), dim=1)
        pos_drop = getattr(self, "pos_drop", None)
        hidden = hidden + pos
        if pos_drop is not None:
            hidden = pos_drop(hidden)
        if hasattr(self, "blocks"):
            hidden = self.blocks(hidden)
            hidden = self.norm(hidden)
            return hidden[:, 0]
        if hasattr(self, "attn"):
            hidden = hidden + self.attn(self.norm(hidden))
            return self.norm(hidden)[:, 0]
        raise ValueError("SpikeZIP ViT model expects blocks or attn.")
    if x.dim() == 5:
        hidden = _spikezip_vit_patch_embed(self, x)
        cls_seq, pos_seq = _spikezip_vit_token_sequence(
            self,
            hidden.shape[0],
            hidden.shape[1],
        )
        hidden = torch.cat((cls_seq, hidden), dim=2)
        pos_drop = getattr(self, "pos_drop", None)
        hidden = hidden + pos_seq
        if pos_drop is not None:
            hidden = pos_drop(hidden)
        if hasattr(self, "blocks"):
            hidden = self.blocks(hidden)
            hidden = self.norm(hidden)
            return hidden[:, :, 0]
        if hasattr(self, "attn"):
            hidden = hidden + self.attn(self.norm(hidden))
            return self.norm(hidden)[:, :, 0]
        raise ValueError("SpikeZIP ViT model expects blocks or attn.")
    raise ValueError("SpikeZIP ViT forward expects [B,C,H,W] or [T,B,C,H,W].")


def _spikezip_vit_forward(self: nn.Module, x: torch.Tensor) -> torch.Tensor:
    return self.head(self.forward_features(x))


def _patch_spikezip_vit_forward(model: nn.Module) -> None:
    if not all(
        hasattr(model, name)
        for name in ("patch_embed", "cls_token", "pos_embed", "head")
    ):
        return
    model._spikezip_t = 0
    original_reset = getattr(model, "reset", None)

    def _spikezip_vit_reset(self: nn.Module) -> None:
        self._spikezip_t = 0
        if original_reset is not None:
            original_reset()

    model.reset = types.MethodType(_spikezip_vit_reset, model)
    model.forward_features = types.MethodType(_spikezip_vit_forward_features, model)
    model.forward = types.MethodType(_spikezip_vit_forward, model)


def _validate_spikezip_vit_model(model: nn.Module) -> None:
    has_top_level_vit = all(
        hasattr(model, name)
        for name in ("patch_embed", "cls_token", "pos_embed", "head")
    )
    if not has_top_level_vit:
        return
    if not hasattr(model, "norm"):
        raise ValueError("SpikeZIP ViT model expects a top-level norm module.")
    if not (hasattr(model, "blocks") or hasattr(model, "attn")):
        raise ValueError("SpikeZIP ViT model expects top-level blocks or attn.")


[文档] class SpikeZIPTFQANNRecipe(ModuleConversionRecipe): def __init__( self, time_steps: int = 200, model_family: str = "roberta", strict: bool = True, ) -> None: r""" **API Language** - :ref:`中文 <SpikeZIPTFQANNRecipe.__init__-cn>` | :ref:`English <SpikeZIPTFQANNRecipe.__init__-en>` ---- .. _SpikeZIPTFQANNRecipe.__init__-cn: * **中文** SpikeZIP-TF QANN-to-SNN recipe。该 recipe 不执行 ANN 量化或后训练, 只把已经兼容 SpikeZIP 的 QANN 原位替换为透明 SNN module。当前版本支持 ``"roberta"`` 与 ``"vit"`` 两类已量化模型。RoBERTa-style self-attention module 需要暴露 ``query``、``key``、``value`` linear layers, ``num_attention_heads``、``attention_head_size``、``all_head_size``、 ``dropout``,以及带有 ``s``、``sym``、``pos_max``、``neg_min``、 ``level`` 属性的 ``query_quan``、``key_quan``、``value_quan``、 ``attn_quan``、``after_attn_quan`` quantizers。ViT-style attention module 需要暴露 ``qkv``、``proj``、``quan_q``、``quan_k``、 ``quan_v``、``attn_quan``、``after_attn_quan``、``quan_proj``、 ``num_heads``、``head_dim``、``scale``、``attn_drop`` 与 ``proj_drop``。RoBERTa attention mask 仅在第一个时间步加入,随后由 temporal-difference softmax 的累计状态传播该静态 mask 影响。 若 quantizer 未显式暴露 ``level``,则按 ``pos_max - neg_min + 1`` 推断。 :param time_steps: 记录在转换后模型上的时间步元数据。用户仍需显式构造 单步循环或多步输入序列;该参数不在 recipe 内部编码输入或控制 ``step_mode``。建议不小于 QANN 的量化 ``level``,否则部分 bias 或 残余电荷可能无法完全释放。 :type time_steps: int :param model_family: 模型族。支持 ``"roberta"`` 或 ``"vit"``。 :type model_family: str :param strict: 必须为 ``True``。保留该参数用于未来显式放宽支持边界。 :type strict: bool ---- .. _SpikeZIPTFQANNRecipe.__init__-en: * **English** SpikeZIP-TF QANN-to-SNN recipe. This recipe does not quantize or post-train an ANN; it only converts an already SpikeZIP-compatible QANN into transparent SNN modules. The current version supports ``"roberta"`` and ``"vit"`` quantized models. RoBERTa-style self-attention modules must expose ``query``, ``key`` and ``value`` linear layers, ``num_attention_heads``, ``attention_head_size``, ``all_head_size`` and ``dropout``, plus ``query_quan``, ``key_quan``, ``value_quan``, ``attn_quan`` and ``after_attn_quan`` quantizers with ``s``, ``sym``, ``pos_max``, ``neg_min`` and ``level`` attributes. ViT-style attention modules must expose ``qkv``, ``proj``, ``quan_q``, ``quan_k``, ``quan_v``, ``attn_quan``, ``after_attn_quan``, ``quan_proj``, ``num_heads``, ``head_dim``, ``scale``, ``attn_drop`` and ``proj_drop``. RoBERTa attention masks are added only at the first timestep; the temporal-difference softmax state carries the static mask effect afterwards. If a quantizer does not expose ``level`` explicitly, the recipe infers it as ``pos_max - neg_min + 1``. :param time_steps: Timestep metadata stored on the converted model. Users still explicitly construct single-step loops or multi-step input sequences; the recipe does not encode inputs or control ``step_mode`` internally. It should be no smaller than the QANN quantization ``level``; otherwise some bias terms or residual membrane charge may not be fully emitted. :type time_steps: int :param model_family: Model family. Supported values are ``"roberta"`` and ``"vit"``. :type model_family: str :param strict: Must be ``True``. The parameter is reserved for future explicit boundary relaxation. :type strict: bool """ self.time_steps = time_steps self.model_family = model_family self.strict = strict
[文档] def validate(self, converter: "ModuleConverter") -> None: 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.") if self.model_family not in ("roberta", "vit"): raise ValueError( "SpikeZIPTFQANNRecipe supports model_family='roberta' or 'vit'." ) if self.strict is not True: raise ValueError("SpikeZIPTFQANNRecipe requires strict=True.")
[文档] def convert_module(self, converter: "ModuleConverter", ann: nn.Module) -> nn.Module: model = copy.deepcopy(ann).eval() self._global_level = self._level_from_model(model) self._replace_weight(model) model.ann2snn_recipe = "spikezip_tf_qann" model.time_steps = self.time_steps model.model_family = self.model_family if self.model_family == "vit": _validate_spikezip_vit_model(model) _patch_spikezip_vit_forward(model) return model
def _replace_weight(self, model: nn.Module) -> None: for name, child in list(model.named_children()): replacement = None if self.model_family == "roberta" and _is_roberta_qattention(child): replacement = SpikeZIPRobertaSelfAttention( child, self._level_from_qann(child), ) elif self.model_family == "vit" and _is_vit_qattention(child): replacement = SpikeZIPViTSelfAttention( child, self._level_from_qann(child), ) elif isinstance(child, nn.Embedding): replacement = SpikeZIPEmbedding(child) elif isinstance(child, nn.Conv2d): replacement = SpikeZIPConv2d( child, self._level_from_model(model), bias_steps=1 if self.model_family == "vit" else None, ) elif isinstance(child, nn.Linear): replacement = SpikeZIPLinear( child, self._level_from_model(model), bias_steps=1 if self.model_family == "vit" else None, ) elif isinstance(child, nn.LayerNorm): replacement = SpikeZIPLayerNorm(child) elif self._is_quantizer(child): replacement = STBIFNeuron.from_quantizer(child) elif isinstance(child, nn.ReLU): replacement = nn.Identity() if replacement is None: self._replace_weight(child) else: setattr(model, name, replacement) @staticmethod def _is_quantizer(module: nn.Module) -> bool: return all(hasattr(module, name) for name in ("s", "sym", "pos_max", "neg_min")) @staticmethod def _level_from_qann(module: nn.Module) -> int: for name in ( "query_quan", "key_quan", "value_quan", "quan_q", "quan_k", "quan_v", ): quantizer = getattr(module, name, None) if quantizer is None: continue if hasattr(quantizer, "level"): return int(quantizer.level) if hasattr(quantizer, "pos_max") and hasattr(quantizer, "neg_min"): return int(float(quantizer.pos_max) - float(quantizer.neg_min) + 1) raise ValueError("SpikeZIP QANN attention must expose quantizer level.") def _level_from_model(self, module: nn.Module) -> int: for child in module.modules(): if not self._is_quantizer(child): continue if hasattr(child, "level"): return int(child.level) return int(float(child.pos_max) - float(child.neg_min) + 1) return getattr(self, "_global_level", self.time_steps)