Download _net.py from SpeechAntiSpoofingBenchmarks/Fake-Mamba: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/SpeechAntiSpoofingBenchmarks/Fake-Mamba/resolve/main/_net.py
- Command line
-
hf download hf://SpeechAntiSpoofingBenchmarks/Fake-Mamba/_net.py
-
curl -L -o _net.py https://huggingface.co/SpeechAntiSpoofingBenchmarks/Fake-Mamba/resolve/main/_net.py
13.5 kB
| """Fake-Mamba network (XLS-R 300M + PN-BiMamba encoder), ported from | |
| https://github.com/xuanxixi/Fake-Mamba (no LICENSE upstream) @ e966d2f: | |
| - model.py:SSLModel -> SSLModel | |
| - model.py:Fake_Mamba -> FakeMambaNet | |
| - conformer00.py:PN_BiMambas_Encoder -> PN_BiMambas_Encoder | |
| - conformer00.py:PN_BiMambas -> PN_BiMambas | |
| - mamba_ssm 1.x modules/mamba_simple.Mamba + ops/selective_scan_interface | |
| .selective_scan_ref (vendored subset, as in benchmarks/RawBMamba) -> Mamba, selective_scan_ref | |
| Parameter / module names are identical to the source so the released Exp65 | |
| ``best.pth`` loads with strict=True. | |
| Edits vs. the source (none changes the math): | |
| 1. model.py imports two modules that are not in the repo | |
| (``conformer.ConformerBlock, BiMambaEncoder``; conformer00 imports | |
| ``ConExBiMamba.ExBiMamba``). They are used only by other back-ends, so only | |
| Fake_Mamba + PN_BiMambas are ported; nothing is stubbed. | |
| 2. ``PN_BiMambas(dim=144, depth=12)``: the released checkpoint Exp65 has 12 | |
| PN-BiMamba blocks (encoder.layers.0-11); the source/paper default is 7. | |
| 3. SSLModel builds the wav2vec2 XLS-R 300M architecture from its stored model | |
| config (``xlsr2_300m_cfg.json``, extracted from fairseq's xlsr2_300m.pt and | |
| merged onto the installed Wav2Vec2Config -- the cross-version fix used in | |
| W2V2-AASIST) instead of ``load_model_ensemble_and_task`` on a hard-coded | |
| path; the fine-tuned checkpoint overwrites every weight. ``extract_feat`` | |
| no longer calls ``self.model.train()`` on a device/dtype mismatch (it would | |
| re-enable dropout at inference); the wrapper moves the net to the device | |
| before ``.eval()`` anyway. | |
| 4. Mamba: mamba_ssm's CUDA fast path (``mamba_inner_fn`` / causal_conv1d / | |
| selective_scan_cuda) is replaced by the library's own pure-PyTorch | |
| reference (``use_fast_path=False`` branch: nn.Conv1d + ``selective_scan_ref``), | |
| identical math; runs on every GPU (incl. sm_120) and exports to ONNX/TensorRT. | |
| The inference-cache (step) path is dropped (unused). The time recurrence | |
| is evaluated with a Hillis-Steele parallel prefix scan | |
| (``selective_scan_parallel``; same recurrence, float-rounding-level | |
| differences, ~log2(L) tensor ops instead of an L-step loop); | |
| ``FAKE_MAMBA_SCAN=ref`` selects the library's sequential loop (parity is | |
| checked in the tests). | |
| 5. PN_BiMambas_Encoder runs the forward and the flipped (backward) sequence | |
| through the shared Mamba in ONE call (concatenated along the batch axis) | |
| instead of two calls -- same weights, same per-sample math, half the | |
| Python scan loop. | |
| 6. ``print`` calls removed; ``return_embedding`` kept. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from einops import rearrange, repeat | |
| _DIR = os.path.dirname(os.path.abspath(__file__)) | |
| _XLSR_CFG = os.path.join(_DIR, "xlsr2_300m_cfg.json") | |
| WINDOW = 66800 # source data_utils.py / ASVspoof2019.py: self.cut = 66800 | |
| def pad_repeat(x: np.ndarray, max_len: int = WINDOW) -> np.ndarray: | |
| """source utils.pad: first max_len samples; shorter clips tiled (repeat).""" | |
| x = np.asarray(x, dtype=np.float32).reshape(-1) | |
| n = x.shape[0] | |
| if n >= max_len: | |
| return x[:max_len] | |
| reps = int(max_len / n) + 1 | |
| return np.tile(x, reps)[:max_len].astype(np.float32) | |
| # --------------------------------------------------------------------------- XLS-R | |
| class SSLModel(nn.Module): | |
| def __init__(self, cfg_path: str = _XLSR_CFG): | |
| super().__init__() | |
| from fairseq.dataclass.utils import merge_with_parent | |
| from fairseq.models.wav2vec.wav2vec2 import Wav2Vec2Config, Wav2Vec2Model | |
| from omegaconf import OmegaConf | |
| with open(cfg_path) as f: | |
| stored = OmegaConf.create(json.load(f)) | |
| cfg = merge_with_parent(Wav2Vec2Config(), stored, remove_missing=True) | |
| self.model = Wav2Vec2Model.build_model(cfg, task=None) | |
| self.out_dim = 1024 | |
| def extract_feat(self, input_data): | |
| # source moves the model + calls self.model.train() on device/dtype mismatch: removed (edit 3) | |
| input_tmp = input_data[:, :, 0] if input_data.ndim == 3 else input_data | |
| return self.model(input_tmp, mask=False, features_only=True)["x"] | |
| # --------------------------------------------------------------------------- mamba_ssm v1 subset | |
| def selective_scan_ref(u, delta, A, B, C, D=None, z=None, delta_bias=None, delta_softplus=False): | |
| """mamba_ssm 1.x ops/selective_scan_interface.selective_scan_ref (real A, variable B/C). | |
| u, delta, z: (b, d, l); A: (d, n); B, C: (b, n, l); D, delta_bias: (d,) | |
| """ | |
| dtype_in = u.dtype | |
| u = u.float() | |
| delta = delta.float() | |
| if delta_bias is not None: | |
| delta = delta + delta_bias[..., None].float() | |
| if delta_softplus: | |
| delta = F.softplus(delta) | |
| batch, dim, dstate = u.shape[0], A.shape[0], A.shape[1] | |
| B = B.float() | |
| C = C.float() | |
| x = A.new_zeros((batch, dim, dstate)) | |
| ys = [] | |
| deltaA = torch.exp(torch.einsum("bdl,dn->bdln", delta, A)) | |
| deltaB_u = torch.einsum("bdl,bnl,bdl->bdln", delta, B, u) | |
| for i in range(u.shape[2]): | |
| x = deltaA[:, :, i] * x + deltaB_u[:, :, i] | |
| y = torch.einsum("bdn,bn->bd", x, C[:, :, i]) | |
| ys.append(y) | |
| y = torch.stack(ys, dim=2) # (b d l) | |
| out = y if D is None else y + u * rearrange(D, "d -> d 1") | |
| if z is not None: | |
| out = out * F.silu(z) | |
| return out.to(dtype=dtype_in) | |
| def selective_scan_parallel(u, delta, A, B, C, D=None, z=None, delta_bias=None, delta_softplus=False): | |
| """Same recurrence as ``selective_scan_ref`` (h_t = exp(dt_t A) h_{t-1} + dt_t B_t u_t, | |
| y_t = C_t . h_t), evaluated as a chunked parallel scan: a Hillis-Steele prefix scan | |
| inside chunks of _SCAN_CHUNK steps (log2 levels of pure tensor ops), then a short | |
| sequential carry across chunks -- instead of an L-step Python loop. The combine | |
| (a1, b1) o (a2, b2) = (a1 a2, a2 b1 + b2) only multiplies decays <= 1, so it is | |
| numerically stable; results equal the sequential scan up to float rounding. | |
| """ | |
| dtype_in = u.dtype | |
| u = u.float() | |
| delta = delta.float() | |
| if delta_bias is not None: | |
| delta = delta + delta_bias[..., None].float() | |
| if delta_softplus: | |
| delta = F.softplus(delta) | |
| L = u.shape[2] | |
| K = _SCAN_CHUNK | |
| nc = -(-L // K) | |
| # time-major layout (l b d n): shifted slices are contiguous blocks | |
| a = torch.exp(torch.einsum("bdl,dn->lbdn", delta, A)) | |
| h = torch.einsum("bdl,bnl,bdl->lbdn", delta, B.float(), u) | |
| if nc * K != L: # pad the tail with identity steps (a=1, h=0); truncated below | |
| a = torch.cat([a, a.new_ones((nc * K - L,) + a.shape[1:])]) | |
| h = torch.cat([h, h.new_zeros((nc * K - L,) + h.shape[1:])]) | |
| a = a.reshape((nc, K) + a.shape[1:]) | |
| h = h.reshape((nc, K) + h.shape[1:]) | |
| # 1) Hillis-Steele inclusive scan inside each chunk (dim 1): log2(K) levels | |
| k = 1 | |
| while k < K: | |
| # positions < k have no earlier state in the chunk; a -> within-chunk prefix product | |
| h = torch.cat([h[:, :k], h[:, k:] + a[:, k:] * h[:, :-k]], dim=1) | |
| a = torch.cat([a[:, :k], a[:, k:] * a[:, :-k]], dim=1) | |
| k *= 2 | |
| # 2) carry the state across chunks (nc short steps), then 3) add it everywhere | |
| carry = [h.new_zeros(h.shape[2:])] | |
| for c in range(nc - 1): | |
| carry.append(h[c, -1] + a[c, -1] * carry[-1]) | |
| h = h + a * torch.stack(carry)[:, None] | |
| h = h.reshape((nc * K,) + h.shape[2:])[:L] | |
| y = torch.einsum("lbdn,bnl->bdl", h, C.float()) | |
| out = y if D is None else y + u * rearrange(D, "d -> d 1") | |
| if z is not None: | |
| out = out * F.silu(z) | |
| return out.to(dtype=dtype_in) | |
| _SCAN_CHUNK = 8 # 2026-10-30: fastest of 8/16/32 on RTX 4070 Ti SUPER (B=16: 127 vs 143 vs 194 ms per encoder pass) | |
| # "parallel" (default; fast, exportable) or "ref" (the library's sequential reference loop) | |
| SCAN_IMPL = os.environ.get("FAKE_MAMBA_SCAN", "parallel") | |
| class Mamba(nn.Module): | |
| """mamba_ssm 1.x modules/mamba_simple.Mamba (forward, use_fast_path=False branch).""" | |
| def __init__(self, d_model, d_state=16, d_conv=4, expand=2, dt_rank="auto", | |
| dt_min=0.001, dt_max=0.1, dt_init="random", dt_scale=1.0, dt_init_floor=1e-4, | |
| conv_bias=True, bias=False): | |
| super().__init__() | |
| self.d_model = d_model | |
| self.d_state = d_state | |
| self.d_conv = d_conv | |
| self.expand = expand | |
| self.d_inner = int(self.expand * self.d_model) | |
| self.dt_rank = math.ceil(self.d_model / 16) if dt_rank == "auto" else dt_rank | |
| self.in_proj = nn.Linear(self.d_model, self.d_inner * 2, bias=bias) | |
| self.conv1d = nn.Conv1d(self.d_inner, self.d_inner, bias=conv_bias, kernel_size=d_conv, | |
| groups=self.d_inner, padding=d_conv - 1) | |
| self.activation = "silu" | |
| self.act = nn.SiLU() | |
| self.x_proj = nn.Linear(self.d_inner, self.dt_rank + self.d_state * 2, bias=False) | |
| self.dt_proj = nn.Linear(self.dt_rank, self.d_inner, bias=True) | |
| # (init values are irrelevant: every tensor is overwritten by the checkpoint) | |
| A = repeat(torch.arange(1, self.d_state + 1, dtype=torch.float32), "n -> d n", d=self.d_inner) | |
| self.A_log = nn.Parameter(torch.log(A).contiguous()) | |
| self.D = nn.Parameter(torch.ones(self.d_inner)) | |
| self.out_proj = nn.Linear(self.d_inner, self.d_model, bias=bias) | |
| def forward(self, hidden_states): | |
| batch, seqlen, dim = hidden_states.shape | |
| xz = rearrange(self.in_proj.weight @ rearrange(hidden_states, "b l d -> d (b l)"), | |
| "d (b l) -> b d l", l=seqlen) | |
| if self.in_proj.bias is not None: | |
| xz = xz + rearrange(self.in_proj.bias.to(dtype=xz.dtype), "d -> d 1") | |
| A = -torch.exp(self.A_log.float()) # (d_inner, d_state) | |
| x, z = xz.chunk(2, dim=1) | |
| x = self.act(self.conv1d(x)[..., :seqlen]) | |
| x_dbl = self.x_proj(rearrange(x, "b d l -> (b l) d")) # (bl d) | |
| dt, B, C = torch.split(x_dbl, [self.dt_rank, self.d_state, self.d_state], dim=-1) | |
| dt = self.dt_proj.weight @ dt.t() | |
| dt = rearrange(dt, "d (b l) -> b d l", l=seqlen) | |
| B = rearrange(B, "(b l) dstate -> b dstate l", l=seqlen).contiguous() | |
| C = rearrange(C, "(b l) dstate -> b dstate l", l=seqlen).contiguous() | |
| scan = selective_scan_parallel if SCAN_IMPL == "parallel" else selective_scan_ref | |
| y = scan(x, dt, A, B, C, self.D.float(), z=z, | |
| delta_bias=self.dt_proj.bias.float(), delta_softplus=True) | |
| y = rearrange(y, "b d l -> b l d") | |
| return self.out_proj(y) | |
| # --------------------------------------------------------------------------- PN-BiMamba | |
| class PN_BiMambas_Encoder(nn.Module): | |
| def __init__(self, d_model, n_state): | |
| super().__init__() | |
| self.d_model = d_model | |
| self.mamba = Mamba(d_model, n_state) | |
| self.norm1 = nn.LayerNorm(d_model) | |
| self.norm2 = nn.LayerNorm(d_model) | |
| self.feed_forward = nn.Sequential( | |
| nn.Linear(d_model, d_model * 4), | |
| nn.GELU(), | |
| nn.Linear(d_model * 4, d_model), | |
| ) | |
| def forward(self, x): | |
| residual = x | |
| x_norm = self.norm1(x) | |
| b = x_norm.shape[0] | |
| # forward + backward (flipped) through the shared Mamba in one call (edit 5) | |
| both = self.mamba(torch.cat([x_norm, torch.flip(x_norm, dims=[1])], dim=0)) | |
| mamba_out_forward = both[:b] | |
| mamba_out_backward = torch.flip(both[b:], dims=[1]) | |
| mamba_out = mamba_out_forward + mamba_out_backward | |
| mamba_out = self.norm2(mamba_out) | |
| ff_out = self.feed_forward(mamba_out) | |
| return ff_out + residual | |
| class PN_BiMambas(nn.Module): | |
| def __init__(self, dim, *, depth): | |
| super().__init__() | |
| self.dim = dim | |
| self.layers = nn.ModuleList([PN_BiMambas_Encoder(d_model=dim, n_state=16) for _ in range(depth)]) | |
| def forward(self, x): | |
| for block in self.layers: | |
| x = block(x) | |
| return x | |
| class FakeMambaNet(nn.Module): | |
| """model.py:Fake_Mamba (args.emb_size = 144). forward(x[B, T]) -> logits[B, 2].""" | |
| def __init__(self, emb_size: int = 144, depth: int = 12): | |
| super().__init__() | |
| self.ssl_model = SSLModel() | |
| self.LL = nn.Linear(1024, emb_size) | |
| self.first_bn = nn.BatchNorm2d(num_features=1) | |
| self.selu = nn.SELU(inplace=True) | |
| self.encoder = PN_BiMambas(dim=144, depth=depth) | |
| self.attention_pool = nn.Linear(144, 1) | |
| self.fc5 = nn.Linear(144, 2) | |
| def forward(self, x, return_embedding=False): | |
| x_ssl_feat = self.ssl_model.extract_feat(x.squeeze(-1)) # (B, T, 1024) | |
| x = self.LL(x_ssl_feat) # (B, T, 144) | |
| x = x.unsqueeze(dim=1) | |
| x = self.first_bn(x) | |
| x = self.selu(x) | |
| x = x.squeeze(dim=1) | |
| x = self.encoder(x) | |
| attention_weights = F.softmax(self.attention_pool(x), dim=1) # (B, T, 1) | |
| x_pooled = torch.matmul(attention_weights.transpose(-1, -2), x).squeeze(-2) # (B, 144) | |
| if return_embedding: | |
| return self.fc5(x_pooled), x_pooled | |
| return self.fc5(x_pooled) | |
| def load_state(path: str) -> dict: | |
| sd = torch.load(path, map_location="cpu") | |
| return sd.get("state_dict", sd) if isinstance(sd, dict) and "state_dict" in sd else sd | |