Conversation
Masks are only used by the pooling layer of speaker embedding models: the layers before it, which account for nearly all of the compute, do not depend on them. `get_embeddings` however called the embedding model once per (chunk, speaker) pair, feeding it the very same waveform `num_speakers` times. Embedding models supporting (batch, speakers, frames)-shaped masks are now called once per chunk, with the masks of all its speakers at once. `StatsPool` already pools the very same sequence of features once per speaker, so the output is unchanged. `embedding_batch_size` keeps counting (chunk, speaker) pairs, so peak memory is unchanged as well. Support is declared by `BaseInference.supports_multi_speaker_masks`, `False` by default, and decided for pyannote.audio embedding models by running the model once on a dummy batch, in the same spirit as `min_num_samples`. Pipelines relying on SpeechBrain, NeMo or ONNX WeSpeaker embeddings keep the current code path.
|
We arrived at exactly this optimization independently (#2050 / #2051, now closed in favor of this earlier PR) and can contribute an independent, corpus-level verification of its correctness claim. Measured on VoxConverse dev (216 files, 20.3 h, community-1 pipeline, scored with
Maximum absolute embedding difference 2.3e-6 (float noise from batch shape), minimum cosine similarity 0.9999998; final diarization is segment-identical with both automatic and constrained speaker counts. Since the change removes work rather than tuning a kernel, the relative win is largest on CPU, where the embedding step dominates the pipeline by a wide margin. It likely also explains the unexplained "embeddings are ~30x slower than manual extraction" report in #1614: 10x chunk overlap times 3 speaker slots. Two details of this PR we can specifically endorse from our own attempt: probing See also #1996, which contains the same core idea (splitting |
What
get_embeddingscalls the embedding model once per (chunk, speaker) pair, feeding itthe same waveform
num_speakerstimes. Masks only matter to the pooling layer, soeverything before it is computed
num_speakerstimes and discardednum_speakers - 1times.
This PR calls the model once per chunk with the masks of all its speakers at once, for
embedding models that support it. Output is unchanged.
How
BaseInference.supports_multi_speaker_masks(defaults toFalse) tells whether__call__accepts(batch, speakers, frames)-shaped masks.PyannoteAudioPretrainedSpeakerEmbeddinganswers it by running the model once on adummy batch and checking the output shape, the same way
min_num_samplesprobes themodel. This covers models whose post-pooling layers are not agnostic to the extra
dimension, without having to enumerate model classes.
get_embeddingspicks_get_embeddings_per_chunkwhen supported and_get_embeddings_per_pair(the current code, moved as is) otherwise.embedding_batch_sizekeeps counting (chunk, speaker) pairs, so peak memory does notdepend on which implementation runs.
__call__takes 2-dimensional masks, the probe returnsFalsefor them.Results
The number of forward passes through the embedding model drops exactly
num_speakers-fold: 333 → 111 for the file below.pyannote/speaker-diarization-community-1, this repo'ssample.wavrepeated 4× (120s),preloaded as a waveform,
pyannote.audio4.0.7 + torch 2.13, Apple M3 laptop, mediansof 3 runs across several invocations:
Tests
tests/test_speaker_embedding_masks.pybuilds aWeSpeakerResNet34(no download) andchecks that one call with
(batch, speakers, frames)masks returns what severalone-speaker calls return, plus that the capability probe answers
Truefor it.test_stats_pool.py,test_clustering.py,test_sample.pyandtest_import_lib.pyalso pass locally.