File size: 4,083 Bytes
63dd946
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# Copyright (c) 2026 The Anuvani-Diarization authors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""The determinism matrix: the same files through the local engine under changing conditions, every record and RTTM
byte-identical to the raw-audio oracle in every row.

Conditions varied, each level in at least one row: the GPU (each alone, and all together), CPU threads for the
post-processing (1, 2, 8, 64) and for decoding (1, 2, 4, 8), files admitted at once (1, 8, 64 and the default), the
arrival order (a different shuffle per row), a repeat in the same warm process, and a fresh process with every
BLAS and OpenMP thread variable at 64.

Files: the golden 12, 24 more drawn with a fixed seed from the cached sets, and three robustness files (stereo,
44.1 kHz, a file that fails).

  python -s -m pytest -q tests/test_matrix.py -m gpu      # about 15 minutes on idle GPUs
"""
import gc
import json
import os
import random
import subprocess
import sys

import pytest

import records
from conftest import PROJ
from test_identity_audio import INPUTS, compare


def visible_gpus():
    """The number of GPUs this process can use (CUDA_VISIBLE_DEVICES applies); 1 if none can be counted, so that the
    rows still exist and fail where the GPUs are missing."""
    try:
        import torch
        return max(torch.cuda.device_count(), 1)
    except Exception:
        return 1


N_GPU = visible_gpus()
# (post_threads, decode_threads, max_ready) of the single-GPU rows; there are at least as many rows as settings, so that
# every level is in a row on any number of GPUs (a row k runs on GPU k % N_GPU)
THREADS = [(1, 1, 1), (2, 2, 8), (8, 8, 64), (64, 4, 1), (1, 8, 8), (64, 1, 64)]
# (row, devices, post_threads, decode_threads, max_ready)
ROWS = [
    (f"gpu{k}" if k < N_GPU else f"gpu{k % N_GPU}_threads{k}", [f"cuda:{k % N_GPU}"], *THREADS[k % len(THREADS)])
    for k in range(max(N_GPU, len(THREADS)))
] + [("all_gpus", None, 8, 4, None), ("all_gpus_repeat", None, 8, 4, None)]


def matrix_keys():
    keys = [f"bench/{i['sid']}" for i in json.loads((PROJ / "artifacts/golden12_inputs.json").read_text())]
    rng = random.Random(20261008)
    pool = sorted(f"{s}/{p.stem}" for s, p in records.sample(400))
    keys += rng.sample([k for k in pool if k not in keys and k in INPUTS], 24)
    keys += ["robust/stereo_pcm16", "robust/resampled_44100", "robust/contains_nan_float"]
    return list(dict.fromkeys(keys))


def shuffled(keys, seed):
    k = list(keys)
    random.Random(seed).shuffle(k)
    return k


@pytest.fixture(scope="module")
def keys():
    k = matrix_keys()
    assert len(k) == 39 and all(x in INPUTS for x in k)
    return k


@pytest.mark.gpu
@pytest.mark.slow
@pytest.mark.parametrize("row", ROWS, ids=[r[0] for r in ROWS])
def test_matrix_row(keys, row):
    import torch
    from anuvani_diar import Diarizer
    name, devices, post, decode, ready = row
    d = Diarizer(keep_records=True, devices=devices, post_threads=post, decode_threads=decode,
                 max_ready=ready, strict=True)
    try:
        assert compare(shuffled(keys, seed=ROWS.index(row)), d) == []
    finally:
        del d
        gc.collect()
        torch.cuda.empty_cache()


@pytest.mark.gpu
@pytest.mark.slow
def test_matrix_fresh_process_threads_64(keys):
    code = f"""
import json, os, sys
sys.path.insert(0, {str(PROJ / 'tests')!r})
os.chdir({str(PROJ)!r})
from test_identity_audio import compare
from test_matrix import shuffled
from anuvani_diar import Diarizer
keys = json.loads({json.dumps(json.dumps(keys))})
d = Diarizer(keep_records=True, strict=True)
print("BAD", json.dumps(compare(shuffled(keys, seed=99), d)))
"""
    env = dict(os.environ, OPENBLAS_NUM_THREADS="64", OMP_NUM_THREADS="64", MKL_NUM_THREADS="64",
               ANUVANI_DIAR_ANY_BLAS_THREADS="1")
    out = subprocess.run([sys.executable, "-s", "-c", code], env=env, capture_output=True, text=True, timeout=3600)
    lines = [l for l in out.stdout.splitlines() if l.startswith("BAD ")]
    assert lines and json.loads(lines[-1][4:]) == [], out.stdout[-2000:] + out.stderr[-2000:]