StephennFernandes's picture
Anuvani-Diarization 1.0.0
63dd946
Raw History Blame Contribute Delete
10.5 kB
# Copyright (c) 2026 The Anuvani-Diarization authors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""GPU probes (batching, voiceprints, devices): may the network side change anything without changing a single output
bit?
All models are built by the frozen release's own `load_bundle` (canonical flags: FP32, TF32 off, deterministic
kernels, CUBLAS_WORKSPACE_CONFIG=:4096:8), on real IndicDiarBench audio and real cached masks. Every comparison is
bitwise on the float32 outputs. Decision rules (written before running):
batching a row's segmentation output must not change when other rows' content or its position in the batch
change (fixed batch shape); batch-size dependence is reported separately. Cross-file batching is
allowed only if every row is bit-identical across all tested batch sizes.
voiceprints the repacked voiceprint trunk (fbank + ResNet trunk once per window, in batches of 8 distinct windows;
pooling and seg_1 at the reference rows and shapes) is allowed only if every final embedding is
bit-identical.
devices concurrent CUDA streams in one process, and each GPU, must reproduce GPU 0's sequential bits; with
--occupy the reference is repeated with that fraction (for example 0.95) of GPU memory occupied before
the first forward (cuDNN engine fallback).
source env/canonical.sh && CUDA_VISIBLE_DEVICES=0 python -s tools/probes.py batching --out artifacts/probes
"""
import argparse
import hashlib
import json
import os
import sys
import threading
import time
from pathlib import Path
import numpy as np
PROJ = Path(__file__).resolve().parents[1]
REL = PROJ.parent / "anuvani_diar_v1"
SID = PROJ.parents[1]
sys.path.insert(0, str(PROJ / "tests"))
WIN, STEP = 256000, 25600
# golden files (3 languages)
FILES = ["bengali_009", "hindi_033", "kannada_035"]
def load_ref():
import refpkg
ref = refpkg.load()
from anuvani_diar_ref.frontend import load_bundle
return ref, load_bundle
def audio(sid):
import soundfile as sf
p = next((SID / "benchmark_data/indicdiar_full").glob(f"*/{sid}.flac"))
a, sr = sf.read(str(p), dtype="float32")
assert sr == 16000 and a.ndim == 1
return a
def windows(a, n):
"""First n reference windows (chunk i starts at i * 25600; zero-padded if short)."""
out = np.zeros((n, 1, WIN), np.float32)
for i in range(n):
seg = a[i * STEP:i * STEP + WIN]
out[i, 0, :len(seg)] = seg
return out
def masks_for(sid):
import records
p = next(p for s, p in records.all_records() if p.stem == sid)
return np.load(p)["masks"]
def used_weights(masks):
"""pyannote get_embeddings(exclude_overlap=True) mask choice: clean frames if more than 2, else the full mask."""
m = masks.astype(np.float64)
clean = m * (m.sum(axis=2, keepdims=True) < 2)
w = np.where((clean.sum(axis=1, keepdims=True) > 2), clean, m)
# (C, 799, 4)
return np.nan_to_num(w).astype(np.float32)
def sha(t):
return hashlib.sha256(t.detach().cpu().contiguous().numpy().tobytes()).hexdigest()[:16]
def seg_forward(model, x, torch):
with torch.inference_mode():
return torch.softmax(model(torch.from_numpy(x).to("cuda")), -1).cpu()
def emb_parts(emb_model, torch, F):
r = emb_model.resnet
def trunk(w):
fb = emb_model.compute_fbank(w)
x = fb.permute(0, 2, 1)
x = x.unsqueeze_(1)
out = F.relu(r.bn1(r.conv1(x)))
return r.layer4(r.layer3(r.layer2(r.layer1(out))))
def head(out, weights):
return r.seg_1(r.pool(out, weights=weights))
return trunk, head
def batching(args):
import torch
import torch.nn.functional as F
ref, load_bundle = load_ref()
model = load_bundle(REL / "bundle", "cuda:0", 8, 2)
seg = model["pipeline"]._segmentation.model
emb = model["pipeline"]._embedding.model_
trunk, head = emb_parts(emb, torch, F)
res = dict(torch=torch.__version__, cudnn=torch.backends.cudnn.version(), gpu=torch.cuda.get_device_name(0),
deterministic=torch.are_deterministic_algorithms_enabled(), segmentation={}, voiceprints={})
rng = np.random.default_rng(0)
for sid in FILES:
a = audio(sid)
W = windows(a, 24)
# reference: a full batch of 8, in order
base = seg_forward(seg, W[:8], torch)
seg_res = res["segmentation"][sid] = {}
perm = rng.permutation(8)
seg_res["position_change"] = bool(torch.equal(seg_forward(seg, W[:8][perm], torch)[np.argsort(perm)], base))
# other rows replaced by other windows
mixed = W[:8].copy(); mixed[1::2] = W[8:16][1::2]
seg_res["other_rows_content_change"] = bool(torch.equal(seg_forward(seg, mixed, torch)[0::2], base[0::2]))
zero = W[:8].copy(); zero[1::2] = 0.0
seg_res["other_rows_zeroed"] = bool(torch.equal(seg_forward(seg, zero, torch)[0::2], base[0::2]))
seg_res["batch_size_identical"] = {}
for b in (1, 2, 3, 4, 5, 6, 7, 9, 12, 16, 24):
Wb = W[:b] if b <= 24 else W
ob = seg_forward(seg, Wb, torch)
k = min(b, 8)
seg_res["batch_size_identical"][b] = bool(torch.equal(ob[:k], base[:k]))
# voiceprints, reference rows = 2 chunks x 4 speakers (identical crops), real weights
m = used_weights(masks_for(sid))
vp_res = res["voiceprints"][sid] = {}
with torch.inference_mode():
# three reference batches
for start in (0, 2, 6):
crops = torch.from_numpy(np.repeat(W[start:start + 2], 4, axis=0)).to("cuda")
wts = torch.from_numpy(np.stack([m[start + i // 4, :, i % 4] for i in range(8)])).to("cuda")
# what the release computes
ref_emb = emb(crops, weights=wts)
# 8 distinct windows, same shape
distinct = torch.from_numpy(W[start:start + 8]).to("cuda")
T = trunk(distinct)
# gather the two chunks' rows, x4 each
Tg = T[[0, 0, 0, 0, 1, 1, 1, 1]].contiguous()
rep = head(Tg, wts)
T_ref = trunk(crops)
vp_res[f"batch_{start}"] = dict(embedding_identical=bool(torch.equal(rep, ref_emb)),
trunk_rows_identical=bool(torch.equal(T_ref[0], T[0]) and torch.equal(T_ref[4], T[1])),
max_abs_diff=float((rep - ref_emb).abs().max()))
# slot positions: the same window at each of the 8 slots of a distinct batch
pos_ok = True
for slot in range(8):
batch = W[8:16].copy(); batch[slot] = W[0]
T = trunk(torch.from_numpy(batch).to("cuda"))
T0 = trunk(torch.from_numpy(np.repeat(W[0:1], 8, axis=0)).to("cuda"))
pos_ok &= bool(torch.equal(T[slot], T0[0]))
vp_res["trunk_all_slot_positions"] = pos_ok
# tail: the zero-padded last chunk and digital silence
# a short, zero-padded window
tail = windows(a[: 5 * STEP + 70000], 6)[-1:]
silent = np.zeros((1, 1, WIN), np.float32)
for name, w in (("padded_tail", tail), ("digital_silence", silent)):
ref8 = trunk(torch.from_numpy(np.repeat(w, 8, axis=0)).to("cuda"))
mix = W[8:16].copy(); mix[3] = w[0]
got = trunk(torch.from_numpy(mix).to("cuda"))
vp_res[f"trunk_{name}"] = bool(torch.equal(got[3], ref8[0]))
out = Path(args.out); out.mkdir(parents=True, exist_ok=True)
(out / "batching.json").write_text(json.dumps(res, indent=1))
print(json.dumps(res, indent=1))
def devices_reference(args):
"""Sequential reference digests on the visible GPU (also with most of its memory occupied, when --occupy is set)."""
import torch
import torch.nn.functional as F
if args.occupy:
free, total = torch.cuda.mem_get_info()
# held for the run
hog = torch.empty(int(free * args.occupy) // 4, dtype=torch.float32, device="cuda")
ref, load_bundle = load_ref()
model = load_bundle(REL / "bundle", "cuda:0", 8, 2)
seg = model["pipeline"]._segmentation.model
emb = model["pipeline"]._embedding.model_
dig = {}
for sid in FILES:
W = windows(audio(sid), 16)
m = used_weights(masks_for(sid))
with torch.inference_mode():
dig[f"{sid}/seg"] = sha(seg_forward(seg, W[:8], torch))
crops = torch.from_numpy(np.repeat(W[0:2], 4, axis=0)).to("cuda")
wts = torch.from_numpy(np.stack([m[i // 4, :, i % 4] for i in range(8)])).to("cuda")
dig[f"{sid}/emb"] = sha(emb(crops, weights=wts))
if args.streams:
# two threads, each on its own CUDA stream, running the same batches concurrently
outs = {}
def work(name, sid):
W = windows(audio(sid), 16)
s = torch.cuda.Stream()
with torch.cuda.stream(s), torch.inference_mode():
for _ in range(3):
o = torch.softmax(seg(torch.from_numpy(W[:8]).to("cuda", non_blocking=False)), -1)
s.synchronize()
outs[name] = sha(o)
ts = [threading.Thread(target=work, args=(f"t{i}", FILES[i % len(FILES)])) for i in range(4)]
[t.start() for t in ts]; [t.join() for t in ts]
dig["streams"] = {k: v == dig[f"{FILES[int(k[1:]) % len(FILES)]}/seg"] for k, v in outs.items()}
gpu = os.environ.get("CUDA_VISIBLE_DEVICES", "?")
out = Path(args.out); out.mkdir(parents=True, exist_ok=True)
tag = f"devices_gpu{gpu}" + (f"_occupy{args.occupy}" if args.occupy else "") + ("_streams" if args.streams else "")
(out / f"{tag}.json").write_text(json.dumps(dig, indent=1))
print(tag, json.dumps(dig))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("what", choices=["batching", "devices"])
ap.add_argument("--out", default=str(PROJ / "artifacts/probes"))
ap.add_argument("--occupy", type=float, default=0.0)
ap.add_argument("--streams", action="store_true")
a = ap.parse_args()
if os.environ.get("CUBLAS_WORKSPACE_CONFIG") != ":4096:8" or not sys.flags.no_user_site:
raise SystemExit("source env/canonical.sh and run with python -s")
batching(a) if a.what == "batching" else devices_reference(a)
if __name__ == "__main__":
main()