# 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 ` → `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__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 `, `run_trt_jspaw.sh `.