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>.