Download _net.py from SpeechAntiSpoofingBenchmarks/XLSR-Mamba-DF: direct link, hf CLI and curl.
- Browser
- Download file 8.89 kB
-
https://huggingface.co/SpeechAntiSpoofingBenchmarks/XLSR-Mamba-DF/resolve/main/_net.py
- Command line
-
hf download hf://SpeechAntiSpoofingBenchmarks/XLSR-Mamba-DF/_net.py
-
curl -L -o _net.py https://huggingface.co/SpeechAntiSpoofingBenchmarks/XLSR-Mamba-DF/resolve/main/_net.py
8.89 kB
| """XLSR-Mamba network, ported from https://github.com/swagshaw/XLSR-Mamba (MIT) | |
| ``model.py`` (SSLModel, Model) + ``mamba_blocks.py`` (MixerModel, create_block). | |
| DPDA-GA's ``nets/mamba.py`` + ``nets/mamba_blocks.py`` are byte-identical copies. | |
| Module / parameter names are identical to the source so the released state_dicts load | |
| strictly. Edits (inference only; see implementation-notes.md): | |
| 1. SSLModel builds the fairseq wav2vec 2.0 XLS-R 300M architecture from the base | |
| ``xlsr2_300m.pt`` config via merge_with_parent (cross-version fairseq fix, as in | |
| benchmarks/W2V2-AASIST/_net.py) instead of load_model_ensemble_and_task; the | |
| fine-tuned checkpoint then overwrites every weight. | |
| 2. extract_feat: removed the ``self.model.train()`` call (it fires on a device/dtype | |
| mismatch and would re-enable dropout) and the second, unused XLS-R forward | |
| (``layerresult``): the source ran XLS-R twice and discarded the second result. | |
| 3. Model takes the two hyper-parameters directly (emb_size, num_encoders) instead of | |
| argparse ``args``; no ``device`` / print. | |
| 4. Mamba mixer, Block, RMSNorm and the fused add+RMSNorm come from the dir-local | |
| ``_mamba_v1.py`` (pure PyTorch; mamba_ssm v1 subset) instead of the | |
| mamba_ssm CUDA / Triton kernels. Same math. | |
| 5. Only the code path the released checkpoints use is kept (MixerModel with | |
| if_bidirectional=True, fused_add_norm=True, rms_norm=True); BiBlock / BiMixerModel | |
| (unused by Model) are dropped. The quirk of that path is preserved verbatim: the | |
| final fused add+norm adds the *block input* ``hidden_states`` (not the last mixer | |
| output) to each direction's residual. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from dataclasses import dataclass, field | |
| from functools import partial | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| if _HERE not in sys.path: | |
| sys.path.insert(0, _HERE) | |
| from _mamba_v1 import Block, Mamba, RMSNorm, rms_norm_fn # noqa: E402 | |
| WINDOW = 66800 # authors' training/eval cut (4.175 s @ 16 kHz) | |
| _XLSR_PATH = os.path.join(_HERE, "xlsr2_300m.pt") # symlink to W2V2-AASIST's base XLS-R | |
| def pad_repeat(x: np.ndarray, max_len: int = WINDOW) -> np.ndarray: | |
| """Source utils.pad: first max_len samples; shorter clips tiled to max_len.""" | |
| 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) | |
| def load_state(path: str) -> dict: | |
| if path.endswith(".safetensors"): | |
| from safetensors.torch import load_file | |
| return load_file(path, device="cpu") | |
| sd = torch.load(path, map_location="cpu", weights_only=True) | |
| return sd.get("state_dict", sd) | |
| class MambaConfig: | |
| d_model: int = 64 | |
| n_layer: int = 6 | |
| vocab_size: int = 50277 | |
| ssm_cfg: dict = field(default_factory=dict) | |
| rms_norm: bool = True | |
| residual_in_fp32: bool = True | |
| fused_add_norm: bool = True | |
| pad_vocab_size_multiple: int = 8 | |
| class SSLModel(nn.Module): | |
| def __init__(self, cp_path: str = _XLSR_PATH): | |
| super().__init__() | |
| import fairseq | |
| from fairseq.dataclass.utils import merge_with_parent | |
| from fairseq.models.wav2vec.wav2vec2 import Wav2Vec2Config, Wav2Vec2Model | |
| state = fairseq.checkpoint_utils.load_checkpoint_to_cpu(cp_path) | |
| cfg = merge_with_parent(Wav2Vec2Config(), state["cfg"]["model"], remove_missing=True) | |
| ssl = Wav2Vec2Model.build_model(cfg, task=None) | |
| ssl.load_state_dict(state["model"], strict=True) | |
| self.model = ssl | |
| self.out_dim = 1024 | |
| def extract_feat(self, input_data): | |
| input_tmp = input_data[:, :, 0] if input_data.ndim == 3 else input_data | |
| return self.model(input_tmp, mask=False, features_only=True)["x"] | |
| def create_block(d_model, ssm_cfg=None, norm_epsilon=1e-5, rms_norm=False, residual_in_fp32=False, | |
| fused_add_norm=False, layer_idx=None, device=None, dtype=None): | |
| ssm_cfg = ssm_cfg or {} | |
| factory_kwargs = {"device": device, "dtype": dtype} | |
| mixer_cls = partial(Mamba, layer_idx=layer_idx, **ssm_cfg, **factory_kwargs) | |
| norm_cls = partial(nn.LayerNorm if not rms_norm else RMSNorm, eps=norm_epsilon, **factory_kwargs) | |
| block = Block(d_model, mixer_cls, norm_cls=norm_cls, fused_add_norm=fused_add_norm, | |
| residual_in_fp32=residual_in_fp32) | |
| block.layer_idx = layer_idx | |
| return block | |
| class MixerModel(nn.Module): | |
| """mamba_blocks.MixerModel, bidirectional + fused_add_norm path only.""" | |
| def __init__(self, d_model, n_layer, ssm_cfg=None, norm_epsilon=1e-5, rms_norm=False, | |
| fused_add_norm=False, residual_in_fp32=False): | |
| super().__init__() | |
| assert rms_norm and fused_add_norm, "only the released configuration is ported" | |
| self.residual_in_fp32 = residual_in_fp32 | |
| self.fused_add_norm = fused_add_norm | |
| kw = dict(ssm_cfg=ssm_cfg, norm_epsilon=norm_epsilon, rms_norm=rms_norm, | |
| residual_in_fp32=residual_in_fp32, fused_add_norm=fused_add_norm) | |
| self.forward_layers = nn.ModuleList([create_block(d_model, layer_idx=i, **kw) for i in range(n_layer)]) | |
| self.backward_layers = nn.ModuleList([create_block(d_model, layer_idx=i, **kw) for i in range(n_layer)]) | |
| self.norm_f = RMSNorm(d_model, eps=norm_epsilon) | |
| self.f_attention_pool = nn.Linear(d_model, 1) | |
| self.b_attention_pool = nn.Linear(d_model, 1) | |
| self.LL = nn.Linear(d_model * 2, d_model) | |
| self.dropout = nn.Dropout(p=0.1) | |
| self.classifier = nn.Linear(d_model, 2) | |
| def forward(self, x): | |
| hidden_states = self.dropout(x) | |
| f_hidden_states = hidden_states | |
| b_hidden_states = hidden_states.flip([1]) | |
| f_residual, b_residual = None, None | |
| for layer in self.forward_layers: | |
| f_hidden_states, f_residual = layer(f_hidden_states, f_residual) | |
| for layer in self.backward_layers: | |
| b_hidden_states, b_residual = layer(b_hidden_states, b_residual) | |
| # source: "Use long-range features as the key for residual connections" -- | |
| # the fused add+norm takes `hidden_states` (the stack input), not the last mixer output. | |
| f_hidden_states = rms_norm_fn(hidden_states, self.norm_f.weight, self.norm_f.bias, | |
| eps=self.norm_f.eps, residual=f_residual, prenorm=False, | |
| residual_in_fp32=self.residual_in_fp32) | |
| b_hidden_states = rms_norm_fn(hidden_states, self.norm_f.weight, self.norm_f.bias, | |
| eps=self.norm_f.eps, residual=b_residual, prenorm=False, | |
| residual_in_fp32=self.residual_in_fp32) | |
| f_hidden_states = torch.matmul(F.softmax(self.f_attention_pool(f_hidden_states), dim=1) | |
| .transpose(-1, -2), f_hidden_states).squeeze(-2) | |
| b_hidden_states = torch.matmul(F.softmax(self.b_attention_pool(b_hidden_states), dim=1) | |
| .transpose(-1, -2), b_hidden_states).squeeze(-2) | |
| hidden_states = torch.cat((f_hidden_states, b_hidden_states), dim=1) | |
| hidden_states = self.LL(hidden_states) | |
| hidden_states = self.dropout(hidden_states) | |
| return self.classifier(hidden_states) | |
| class Model(nn.Module): | |
| """XLSR-Mamba: XLS-R 300M (fine-tuned) -> Linear 1024->emb -> BN+SELU -> bidirectional Mamba.""" | |
| def __init__(self, emb_size: int = 144, num_encoders: int = 12, cp_path: str = _XLSR_PATH): | |
| super().__init__() | |
| self.ssl_model = SSLModel(cp_path) | |
| self.LL = nn.Linear(1024, emb_size) | |
| self.first_bn = nn.BatchNorm2d(num_features=1) | |
| self.selu = nn.SELU(inplace=True) | |
| self.config = MambaConfig(d_model=emb_size, n_layer=num_encoders // 2) | |
| self.conformer = MixerModel(d_model=self.config.d_model, n_layer=self.config.n_layer, | |
| ssm_cfg=self.config.ssm_cfg, rms_norm=self.config.rms_norm, | |
| residual_in_fp32=self.config.residual_in_fp32, | |
| fused_add_norm=self.config.fused_add_norm) | |
| def set_ref_scan(self, on: bool) -> None: | |
| """Sequential selective_scan_ref (True) vs the chunked scan (False, default).""" | |
| for m in self.modules(): | |
| if isinstance(m, Mamba): | |
| m.use_ref_scan = on | |
| def forward(self, x): | |
| x_ssl_feat = self.ssl_model.extract_feat(x.squeeze(-1)) | |
| x = self.LL(x_ssl_feat) # (bs, frames, emb) | |
| x = x.unsqueeze(dim=1) | |
| x = self.first_bn(x) | |
| x = self.selu(x) | |
| x = x.squeeze(dim=1) | |
| return self.conformer(x) | |