LaST-Net / implementation-notes.md
korallll's picture
Wrapper code (speech-spoof-bench)
f23135c verified
|
Raw History Blame Contribute Delete
6.08 kB
# LaST-Net β€” implementation notes
Plan: `speech-spoof-bench/docs/plans/2026-11-09-thirteen-model-batch-arena-submission.md` (model #3).
## Source
- Code: github.com/JunXue-tech/LaST-Net @ `3838f0c7108313f771f6c1e208ac8f1e3bde60b0` (MIT), clone at
`benchmarks/_ckpt_dl/gh__JunXue-tech__LaST-Net/`: `model.py` (LaSTNet), `infer.py` (single-file scoring),
`eval.py` (dataset eval + `load_model`), `load_dataset.py` (`read_audio`, `crop_or_repeat`).
- Weights: HF JunXueTech/LaST-Net @ `7b18ff0abff078c60745aaaaf5165e3f9542fa1b` `best.pt` (3.8 GB, sha256
`8fee6178d19b398fac9e1b1bfcf13720c4eb5ff722b5b52e4bc698e0de0fa28b`): epoch 52, keys model / optimizer / epoch /
global_step / best_mean_dev_eer (1.383) / args / rng_states / initialization (`xlsr_only`) / pretrained_path /
architecture (`LCF24_LengthSelectiveKernelResidualBiFPN_v18b`) / training_view / consistency. No license β†’ linked.
- Extraction (`extract_state.py`, run once with `python -I`): plain `weights_only=True` fails on the numpy RNG states
(`numpy.dtype[uint32]`); loaded with `weights_only=True` + `torch.serialization.add_safe_globals` limited to
`numpy.core.multiarray._reconstruct`, `numpy.ndarray`, `numpy.dtype`, `_codecs.encode` and the uint32/float64/int64/
float32 dtype classes (the pickle's only globals, checked with `pickletools` first). `ck["model"]` (522 fp32 tensors)
β†’ `_ckpt_dl/JunXueTech__LaST-Net/last_net_model.safetensors` (sha256
`3ec3ff415654931474fe0949b5ed1030a84b8c0b2ee28a4fce37f238e952b264`, metadata: train args), symlinked here.
Train args: router_temperature 0.8, uniform_floor 0.05, layer_dropout 0.1 (= constructor defaults).
- XLS-R architecture: `xlsr2_300m_model_cfg.json` copied from DeepFense-CodecFake-W2V2-MLP (model cfg of fairseq
`xlsr2_300m.pt`); `Wav2Vec2Model.build_model(merge_with_parent(...))` with fairseq 0.12.2 β€” the 3.6 GB base pickle
is not needed (all its tensors are replaced by best.pt anyway, as the authors' README says).
- Paper: none found (web search "LaST-Net Length-Aware Layer and Scale-Adaptive Temporal Network", 2026-11-09) β†’
unpublished tier.
## Wrapper (`last_net.py:LaSTNetModel`, `_net.py:LaSTNet`)
- `_net.py` = model.py verbatim except: SSLModel built from the vendored cfg; forward returns logits only;
`nn.init.zeros_` calls dropped. extract_feat = `model(wav, mask=False, features_only=True, layer=23)["layer_results"]`.
- **Length-aware input**: the model takes `available_seconds` (B,) besides the waveform; `duration_features` builds
[requested/6, log1p(requested)/log 7, available/requested] with requested = 4.0. As infer.py/eval.py:
available = min(len, 64000)/16000 (so a repeated short clip is scored differently from the same tiled audio
given as a full 4 s clip β€” unit-tested).
- **Window**: authors' 4 s = `seconds * 16000` = **64000** samples (not 64600). Prefix crop; shorter β†’ `np.tile` to
ceil(64000/len) copies, cropped. No amplitude normalisation, no resampling. Empty clip β†’ zeros, available 0
(clamped to 1 ms in the model; the source raises instead).
- Score = `log_softmax(logits)[:, 1]` (= `bonafide_log_score`; labels spoof 0 / bona fide 1) β†’ higher = bona fide.
- FP32, TF32 off for cuDNN + matmul (authors' eval.py turns TF32 matmuls on; strict FP32 keeps scores batch-independent).
- Strict load: all 522 tensors, 0 missing / 0 unexpected. Inference-path check (backward on a forward pass): exactly
10 tensors get no gradient = `UNUSED_ON_FORWARD` (mask_emb; quantizer.vars / weight_proj.*; project_q.*;
final_proj.*; encoder.layer_norm.* β€” skipped by fairseq when `layer` is given). Asserted in the tests.
- Params: 318,910,608 loaded; **316,955,664 on the inference path** β†’ meta 316.955664.
## Checks
- `test_last_net.py`: crop/repeat + available seconds, empty clip, duration-feature formula, strict load,
eval mode + param counts, unused set == allow-list, available-duration effect, batch == single (atol 1e-4),
determinism, bona fide > spoof on J-SPAW clips. **11 passed** (idx 3).
- **Source parity** (`source_parity.py <clone>` β†’ `logs_source_parity.txt`): authors' `model.LaSTNet` (fairseq
loader patched to the vendored cfg; same safetensors tensors, strict=True) + `read_audio` / `crop_or_repeat` + the
infer.py loop per clip, vs the wrapper batched, on 16 J-SPAW_LA clips (8 bona fide / 8 spoof) + a 20000, a 5000 and
a 102912-sample clip: max |Ξ”| **1.57e-5 at 4 s**, 1.78e-5 at 6 s (authors' default). (Whole-utterance "window off"
is not an authors' mode: infer/eval only accept 1–6 s.)
- Batch sweep (`sweep.py`, 64000 samples, FP32): RTX 4070 Ti SUPER (idx 2) 117.8 utt/s @24 (plateau 16–64, 118.2 @32)
β†’ `batch_size = 24`; RTX 5060 Ti (idx 3) 41.9 utt/s @4, ~32 at bs β‰₯ 8 β†’ 4.
- Full J-SPAW_LA, PyTorch, RTX 5060 Ti idx 3 (`run_pt.sh`): **EER 14.500%** (2397 trials, 0 skipped). Sane.
## TensorRT (`trt_last_net.py`, sidecar `trt_last-net.json`)
Two-input engine (wav (B,64000) + available (B,)), FP32 with TF32 cleared, profile 1/16/32. Export-only patches:
fairseq `pad_to_multiple` β†’ static pad (199 β†’ 200 frames + mask, as eager); `_net.duration_features` β†’ a trace-safe copy
(`full_like(available, T/16000)` instead of `new_full((len(wav),))`, which would bake the batch size). Build parity on
64 InTheWild clips + 3 truncated (repeat-path) clips.
| card | precision | best batch | utt/s (sweep) | build Spearman / max Ξ” | J-SPAW EER | J-SPAW Spearman vs PT / max Ξ” |
|---|---|---|---|---|---|---|
| RTX 4070 Ti SUPER (idx 2) | FP32 (no TF32) | 16 | 135.6 | 1.0 / 6.1e-5 | 14.500 (=PT) | 1.000000 / 2.7e-4 |
| RTX 5060 Ti (idx 3) | FP32 (no TF32) | 16 | 88.8 | 1.0 / 3.7e-5 | 14.500 (=PT) | 1.000000 / 3.5e-4 |
Both cards pass (|Ξ”EER| = 0 ≀ 0.25, Spearman β‰₯ 0.999) β†’ TRT accepted on both card types. Engines
`engine_<GPU>_fp32_b1-16-32.plan` (gitignored); only the 4070 ONNX is kept. TRT runs in `results_trt2/`, `results_trt3/`
(`cmp_scores.py`, `logs_trt_done.txt`). FP16 not tried (plan: FP32).
Scripts: `run_pt.sh`, `run_trt_export.sh <gpu>`, `run_trt_jspaw.sh <gpu>`.