From 0b1582fc9db20f0c9c0fe38324340a40f17bd8eb Mon Sep 17 00:00:00 2001 From: wd929 Date: Tue, 29 Nov 2022 16:40:54 +0800 Subject: [PATCH 1/6] Add GPU diarisation deployment using triton --- .../Dockerfile/dockerfile.client | 33 ++ .../Dockerfile/dockerfile.server | 38 ++ runtime/server/diarisation_gpu/README.md | 183 +++++++++ runtime/server/diarisation_gpu/bls.png | Bin 0 -> 54538 bytes .../server/diarisation_gpu/client/client.py | 155 +++++++ .../model_repo/clusterer/1/model.py | 163 ++++++++ .../model_repo/clusterer/config.pbtxt | 43 ++ .../diarisation_gpu/model_repo/run/1/model.py | 379 ++++++++++++++++++ .../model_repo/run/config.pbtxt | 43 ++ 9 files changed, 1037 insertions(+) create mode 100644 runtime/server/diarisation_gpu/Dockerfile/dockerfile.client create mode 100644 runtime/server/diarisation_gpu/Dockerfile/dockerfile.server create mode 100644 runtime/server/diarisation_gpu/README.md create mode 100644 runtime/server/diarisation_gpu/bls.png create mode 100644 runtime/server/diarisation_gpu/client/client.py create mode 100644 runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py create mode 100644 runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt create mode 100644 runtime/server/diarisation_gpu/model_repo/run/1/model.py create mode 100644 runtime/server/diarisation_gpu/model_repo/run/config.pbtxt diff --git a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client new file mode 100644 index 00000000..a7f8219d --- /dev/null +++ b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client @@ -0,0 +1,33 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### + +FROM nvcr.io/nvidia/tritonserver:22.07-py3-sdk +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get install -y libsndfile1 +RUN pip3 install soundfile kaldiio +WORKDIR /workspace diff --git a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server new file mode 100644 index 00000000..510593c6 --- /dev/null +++ b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server @@ -0,0 +1,38 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### +FROM nvcr.io/nvidia/tritonserver:22.07-py3 +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get -y install swig && apt-get -y install python3-dev && apt-get install -y cmake +RUN pip3 install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html +RUN pip3 install -v kaldifeat +RUN python3 -m pip install cupy +RUN python3 -m pip install soundfile +RUN pip install cudf-cu11 dask-cudf-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +RUN pip install cuml-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +RUN pip install cugraph-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +WORKDIR /workspace diff --git a/runtime/server/diarisation_gpu/README.md b/runtime/server/diarisation_gpu/README.md new file mode 100644 index 00000000..7bdb09cf --- /dev/null +++ b/runtime/server/diarisation_gpu/README.md @@ -0,0 +1,183 @@ +# Best Practice for Deploying a WeSpeaker diarisation service using Triton + +In this best practice, we'll go through how to deploy a WeSpeaker diarisation pipeline in GPU by using NVIDIA [Triton Inference Server](https://github.com/triton-inference-server/server), which contains several modules including SAD, Speaker Embedding Extraction, Clustering and etc. + +We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. + +## Table of Contents + +- [Preparation](#preparation) + - [Prepare Environment](#prepare-environment) + - [Prepare Models](#prepare-models) + - [Preapare Test Data](#prepare-test-data) +- [Triton Inference Server](#triton-inference-server) + - [Quick Start](#quick-start) + - [Business Logic Scripting](#bls) +- [Inference Client](#inference-client) + - [Quick Start](#quick-start-1) + - [Compute Metrics](#compute-metrics) +- [Benchmark](#benchmark) + + +## Preparation + +Let's prepare enrivonments, models and data first. + +### Prepare Environment + +Clone the repository: + +```bash +# Clond WeSpeaker repo +git clone https://github.com/wenet-e2e/wespeaker.git +export WeSpeaker=$PWD/wespeaker/ +cd runtime??? +export PROJECT_DIR=$PWD + +``` + +### Prepare Models + +To depoloy this pipeline, first we should obtain SAD and Speaker models. + +#### Speaker Models + +You can refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) to train a WeSpeaker model or use a pre-trained model: + +```bash +export SPK_MODEL_DIR=/workspace/pretrained_models +mkdir -p ${SPK_MODEL_DIR} +wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O ${SPK_MODEL_DIR}/voxceleb_resnet34_LM.onnx +``` + +Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. +After that, speaker models will be avaliable in `wespeaker/runtime/server/x86_gpu/model_repo/` directory. + +```bash +export SPK_MODEL_REPO="wespeaker/runtime/server/x86_gpu/model_repo/" +``` + +#### SAD Models + +Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). + +```bash +export SAD_DIR=/workspace/SAD +wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v3.1.zip -O external_tools/silero-vad-v3.1.zip +unzip -o external_tools/silero-vad-v3.1.zip -d external_tools +cp external_tools/silero-vad-3.1/files/silero_vad.jit $SAD_DIR/ +``` + +### Prepare Test Data + +You can use the following command to access the evluation datas from voxconverse: + +bash +``` +bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 +``` + +If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, + +``` +abjxc abjxc.wav +afjiv afjiv.wav +``` + +## Triton Inference Server + +[Triton Inference Server](https://github.com/triton-inference-server/server) can help with the most of serving work for us and handles requests/results sending and receiving, request scheduling, load balance, and inference execution. In this section, we will use Triton to depoy the diarisation pipeline. + +![Pipeline](./bls.png) + +Build the server docker image: +``` +docker build . -f Dockerfile/dockerfile.server -t wespeaker_server:latest --network host +``` + +Build the client docker image: +``` +docker build . -f Dockerfile/dockerfile.client -t wespeaker_client:latest --network host +``` + +Run the following commands to put the pretrained SAD and Speaker models into current `model_repo` directory. + +```bash +cd ${PROJECT_DIR} + +mkdir -p model_repo/run/1 + +cp -r $SPK_MODEL_REPO/* model_repo/ + +``` + +### Quick Start + +Now start server: + +```bash +# Start the docker container +docker run --gpus all -v $PWD/model_repo:/workspace/model_repo -v $SAD_DIR:/workspace/triton/ --net host --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -it wespeaker_server:latest + +# Inside the docker container +tritonserver --model-repository=/workspace/model_repo + +``` + +### Business Logic Scripting + +Business Logic Scripting (BLS) can execute inference requests on other models being served by Triton as a part of executing one Python model. + + +## Inference Client + +In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. + + +### Quick Start + +Run, + +```bash +AUDIO_DATA= +docker run -ti --net host -v $PWD/client:/ws/client -v $AUDIO_DATA:/ws/test_data wespeaker_client:latest +cd /ws/client +``` + +In the docker container, run the client script to do the whole pipeline inference. + +```bash +# Test one audio +export output_directory="output" +mkdir -p $output_directory +python client.py --url=localhost:8001 --audio_file=/ws/test_data/abjxc.wav --output_directory=$output_directory +cat $output_directory/rttm* > $output_directory/rttm +``` + +The above command sends a single audio `abjxc.wav` to the server and get the result. `--url` option specifies the IP and port of the server, in our example, we set the server and client on the same machine, therefore IP is `localhost`, and we use port `8001` since it is the default port for gRPC in Triton. But if your client is not on the same machine as the server, you should change this option. + +You can also test specify the path of `wav.scp` with `--wavscp` option, then the client will test the audio files in the `wav.scp`. + +```bash +# Test a bunch of audios +export wav_scp_dir=/ws/test_data +python client.py --url=localhost:8001 --wavscp=$wav_scp_dir/wav.scp --output_directory="outp" +cat $output_directory/rttm* > $output_directory/rttm +``` + +Finally, you can get the RTTM information in `$output_directory/rttm`. + +### Compute Metrics + +If you want to test the performances of our SD pipeline, you can run: + +bash +``` +perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat data/voxconverse-master/dev/*.rttm) \ + -s $output_directory/rttm +``` + +## Benchmark (TODO) + diff --git a/runtime/server/diarisation_gpu/bls.png b/runtime/server/diarisation_gpu/bls.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd15d493acd599c6f2c3ccbed7d0645c92a9238 GIT binary patch literal 54538 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 BvC#kk literal 0 HcmV?d00001 diff --git a/runtime/server/diarisation_gpu/client/client.py b/runtime/server/diarisation_gpu/client/client.py new file mode 100644 index 00000000..041bb54d --- /dev/null +++ b/runtime/server/diarisation_gpu/client/client.py @@ -0,0 +1,155 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +from multiprocessing import Pool + +import tritonclient.grpc as grpcclient +from tritonclient.utils import np_to_triton_dtype +import numpy as np +import soundfile +import argparse +import os + + +class SpeakerClient(object): + def __init__(self, triton_client, model_name, protocol_client): + self.triton_client = triton_client + self.protocol_client = protocol_client + self.model_name = model_name + + def recognize(self, wav_path, client_index): + # We send batchsize=1 data to server + # BatchSize > 1 is also ok but you need to take care of + # padding. + waveform, sample_rate = soundfile.read(wav_path) + cur_length = len(waveform) + input = np.zeros((1, cur_length), dtype=np.float32) + input[0][0:cur_length] = waveform[0:cur_length] + inputs = [self.protocol_client.InferInput("input", input.shape, + np_to_triton_dtype(input.dtype))] + inputs[0].set_data_from_numpy(input) + outputs = [grpcclient.InferRequestedOutput("LABELS")] + response = self.triton_client.infer(self.model_name, + inputs, + request_id=str(client_index), + outputs=outputs) + result = response.as_numpy("LABELS")[0] + return [result] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-v', + '--verbose', + action="store_true", + required=False, + default=False, + help='Enable verbose output') + parser.add_argument('-u', + '--url', + type=str, + required=False, + default='localhost:8001', + help='Inference server URL. Default is ' + 'localhost:8001.') + parser.add_argument('--model_name', + required=False, + default='run', + help='the model to send request to') + parser.add_argument('--wavscp', + type=str, + required=False, + default=None, + help='audio_id \t absolute_wav_path') + parser.add_argument('--output_directory', + type=str, + required=False, + default=None, + help='the path to save the segment files') + parser.add_argument('--data_dir', + type=str, + required=False, + default=None, + help='data dir will be append to audio file if given') + parser.add_argument('--audio_file', + type=str, + required=False, + default=None, + help='single wav file') + FLAGS = parser.parse_args() + + # load data + audio_wavpath = [] + if FLAGS.audio_file is not None: + path = FLAGS.audio_file + if FLAGS.data_dir: + path = os.path.join(FLAGS.data_dir, path) + if os.path.exists(path): + audio_wavpath = [(FLAGS.audio_file, path)] + elif FLAGS.wavscp is not None: + with open(FLAGS.wavscp, "r", encoding="utf-8") as f: + for line in f: + aid, path = line.strip().split(' ') + audio_wavpath.append((aid, path)) + + num_workers = multiprocessing.cpu_count() // 2 + + def single_job(li): + idx, audio_files = li + dir_name = os.path.dirname(FLAGS.output_directory) # get the path + if not os.path.exists(dir_name) and (dir_name != ''): + os.makedirs(dir_name) + seg_writer = open(os.path.join(FLAGS.output_directory, + 'rttm'+str(idx)), 'w', encoding="utf-8") + + with grpcclient.InferenceServerClient( + url=FLAGS.url, + verbose=FLAGS.verbose) as triton_client: + protocol_client = grpcclient + speech_client = SpeakerClient(triton_client, FLAGS.model_name, + protocol_client) + + predictions = {} + + for li in audio_files: + utt, wavpath = li + rttms = speech_client.recognize(wavpath, idx)[0] + spec = "SPEAKER {} {} {:.3f} {:.3f} {} " + for i in range(0, rttms.shape[0]): + begin = rttms[i][0] + end = rttms[i][1] + label = int(rttms[i][2]) + channel = 1 + seg_writer.write(spec.format(utt, + channel, + begin, + end - begin, + label)+'\n') + seg_writer.flush() + return predictions + + # start to do inference + # Group requests in batches + predictions = [] + tasks = [] + splits = np.array_split(audio_wavpath, num_workers) + + for idx, per_split in enumerate(splits): + cur_files = per_split.tolist() + tasks.append((idx, cur_files)) + + with Pool(processes=num_workers) as pool: + prediction = pool.map(single_job, tasks) diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py b/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py new file mode 100644 index 00000000..606e05c9 --- /dev/null +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py @@ -0,0 +1,163 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import from_dlpack +import json +import cupy as cp +import numpy as np +from cuml.cluster import KMeans as cuKM + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + * device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name( + model_config, "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + def cluster_gpu(self, embeddings, p=.01, num_spks=None, + min_num_spks=1, max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / cp.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + cp.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + for i in range(m): + indexes = cp.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[cp.diag_indices(M.shape[0])] = 0.0 + D = cp.diag(cp.sum(cp.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = cp.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else cp.argmax(cp.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + kmeans_float = cuKM(n_clusters=k, n_init=10, random_state=10) + kmeans_float.fit(cp.asarray(data)) + return kmeans_float.labels_ + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(embeddings) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, + min_num_spks, max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + + Returns + ------- + list + A list of pb_utils.InferenceResponse. + The length of this list must be the same as `requests` + """ + batch_count = [] + total_embd = [] + + responses = [] + for request in requests: + # the requests will all have the same shape + # different shape request will be + # separated by triton inference server + input0 = pb_utils.get_input_tensor_by_name(request, "EMBEDDINGS") + cur_b_embd = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_embd.shape[0] + batch_count.append(cur_batch) + + for embds in cur_b_embd: + total_embd.append(embds.to(self.device)) + + labels_list = [] + for embds in total_embd: + res = self.cluster_gpu(cp.asarray(embds)) + labels_list.append(cp.asnumpy(res)) + + idx = 0 + for b in batch_count: + batch_labels = np.array(labels_list[idx:idx + b]) + idx += b + out0 = pb_utils.Tensor("LABELS", + batch_labels.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + return responses diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt new file mode 100644 index 00000000..06b0652e --- /dev/null +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "clusterer" +backend: "python" +max_batch_size: 256 + +input [ + { + name: "EMBEDDINGS" + data_type: TYPE_FP32 + dims: [ -1, 256 ] # embedding dim + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_INT32 + dims: [ -1 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] diff --git a/runtime/server/diarisation_gpu/model_repo/run/1/model.py b/runtime/server/diarisation_gpu/model_repo/run/1/model.py new file mode 100644 index 00000000..e9a0f2b1 --- /dev/null +++ b/runtime/server/diarisation_gpu/model_repo/run/1/model.py @@ -0,0 +1,379 @@ +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import to_dlpack, from_dlpack +import torch +import numpy as np +import json +import asyncio + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name( + model_config, "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + self.init_jit_model("/workspace/triton/silero_vad.jit") + + def init_jit_model(self, model_path): + torch.set_grad_enabled(False) + self.sad_model = torch.jit.load(model_path, map_location=self.device) + self.sad_model.eval() + + def prepare_chunks(self, + wav, + audio_length_samples, + sr: int = 16000, + window_size_samples: int = 1536): + chunks = [] + self.sad_model.reset_states() + + for current_start_sample in range(0, audio_length_samples, + window_size_samples): + chunk = wav[current_start_sample: + current_start_sample + window_size_samples] + if len(chunk) < window_size_samples: + chunk = torch.nn.functional.pad( + chunk, + (0, int(window_size_samples - len(chunk)))) + speech_prob = self.sad_model(chunk, 16000) + chunks.append(speech_prob) + return chunks + + def get_timestamps(self, speech_probs, audio_length_samples, + sr: int = 16000, + threshold: float = 0.5, + min_duration: float = 0.255, + min_speech_duration_ms: int = 250, + min_silence_duration_ms: int = 100, + window_size_samples: int = 1536, + speech_pad_ms: int = 30): + triggered = False + speeches = [] + current_speech = {} + neg_threshold = threshold - 0.15 + temp_end = 0 + + min_speech_samples = sr * min_speech_duration_ms / 1000 + min_silence_samples = sr * min_silence_duration_ms / 1000 + speech_pad_samples = sr * speech_pad_ms / 1000 + + for i, speech_prob in enumerate(speech_probs): + if (speech_prob >= threshold) and temp_end: + temp_end = 0 + + if (speech_prob >= threshold) and not triggered: + triggered = True + current_speech['start'] = window_size_samples * i + continue + + if (speech_prob < neg_threshold) and triggered: + if not temp_end: + temp_end = window_size_samples * i + if (window_size_samples * i) - temp_end < min_silence_samples: + continue + else: + current_speech['end'] = temp_end + if (current_speech['end'] - + current_speech['start']) > min_speech_samples: + speeches.append(current_speech) + temp_end = 0 + current_speech = {} + triggered = False + continue + if current_speech: + current_speech['end'] = audio_length_samples + speeches.append(current_speech) + + for i, speech in enumerate(speeches): + if i == 0: + speech['start'] = int(max(0, + speech['start'] - speech_pad_samples)) + if i != len(speeches) - 1: + silence_duration = speeches[i+1]['start'] - speech['end'] + if silence_duration < 2 * speech_pad_samples: + speech['end'] += int(silence_duration // 2) + speeches[i+1]['start'] = int(max(0, speeches[i+1]['start'] + - silence_duration // 2)) + else: + speech['end'] += int(speech_pad_samples) + else: + speech['end'] = int(min(audio_length_samples, speech['end'] + + speech_pad_samples)) + vad_result = [] + for item in speeches: + begin = item['start'] / sr + end = item['end'] / sr + if end - begin >= min_duration: + item['start'] = begin + item['end'] = end + vad_result.append(item) + return vad_result + + def subsegment(self, wav, segments, wav_idx, + window_fs: float = 1.50, + period_fs: float = 0.75, + sr: int = 16000, + frame_shift: int = 10): + def repeat_to_fill(x, window_fs): + length = x.size(0) + num = (window_fs + length - 1) // length + + x = x.repeat(1, num)[0][:window_fs] + input = torch.zeros((1, window_fs), device=self.device) + input[0] = x + return input + + subsegs = [] + subseg_signals = [] + + seg_idx = 0 + + window_fs = int(window_fs * sr) + period_fs = int(period_fs * sr) + for segment in segments: + seg_begin, seg_end = int(segment['start'] * sr) + seg_end = int(segment['end'] * sr) + seg_signal = wav[seg_begin: seg_end + 1] + seg_length = seg_end - seg_begin + + if seg_length <= window_fs: + subseg = [wav_idx, seg_idx, + segment['start'], segment['end'], 0, + int(seg_length / sr * 1000 // frame_shift)] + subseg_signal = repeat_to_fill(seg_signal, window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + else: + max_subseg_begin = seg_length - window_fs + period_fs + for subseg_begin in range(0, max_subseg_begin, period_fs): + subseg_end = min(subseg_begin + window_fs, seg_length) + subseg = [wav_idx, seg_idx, + segment['start'], segment['end'], + int(subseg_begin / sr * 1000 / frame_shift), + int(subseg_end / sr * 1000 / frame_shift)] + subseg_signal = repeat_to_fill( + seg_signal[subseg_begin: subseg_end + 1], window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + + return subsegs, subseg_signals + + def read_labels(self, subseg_ids, label, frame_shift=10): + utt_to_subseg_labels = [] + new_sort = {} + for i, subseg in enumerate(subseg_ids): + (utt, seg_idx, begin_ms, end_ms, begin_frames, end_frames) = subseg + begin = (int(begin_ms) + int(begin_frames) * frame_shift) / 1000.0 + end = (int(begin_ms) + int(end_frames) * frame_shift) / 1000.0 + new_sort[seg_idx] = (begin, end, label[i]) + utt_to_subseg_labels = list(dict(sorted(new_sort.items())).values()) + return utt_to_subseg_labels + + def merge_segments(self, subseg_to_labels): + merged_segment_to_labels = [] + + if len(subseg_to_labels) == 0: + return merged_segment_to_labels + + (begin, end, label) = subseg_to_labels[0] + for (b, e, la) in subseg_to_labels[1:]: + if b <= end and la == label: + end = e + elif b > end: + merged_segment_to_labels.append((begin, end, label)) + begin, end, label = b, e, la + elif b <= end and la != label: + pivot = (b + end) / 2.0 + merged_segment_to_labels.append((begin, pivot, label)) + begin, end, label = pivot, e, la + else: + raise ValueError + merged_segment_to_labels.append((begin, e, label)) + + return merged_segment_to_labels + + async def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + Returns + ------- + list + A list of pb_utils.InferenceResponse. The length of this list must + be the same as `requests` + """ + + batch_count = [] + batch_len = [] + + total_wavs = [] + total_lens = [] + responses = [] + + for request in requests: + input0 = pb_utils.get_input_tensor_by_name(request, "input") + + cur_b_wav = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_wav.shape[0] + cur_len = cur_b_wav.shape[1] + batch_count.append(cur_batch) + batch_len.append(cur_len) + + for wav in cur_b_wav: + total_lens.append(len(wav)) + total_wavs.append(wav.to(self.device)) + + speech_shapes = [] + all_probs = [] + + for wav, lens in zip(total_wavs, total_lens): + chunks = self.prepare_chunks(wav, lens) + speech_shapes.append(len(chunks)) + all_probs.append(chunks) + reshape_probs = [] + idx = 0 + for i in range(0, len(speech_shapes)): + cur_speech = [] + for j in range(0, speech_shapes[i]): + cur_speech.append(all_probs[i][j]) + idx += 1 + reshape_probs.append(cur_speech) + + out_segs = [] + for speech_prob, speech_len in zip(reshape_probs, total_lens): + segments = self.get_timestamps(speech_prob, + speech_len, threshold=0.36) + out_segs.append(segments) + + total_subsegments = [] + total_subsegment_ids = [] + total_embds = [] + + wav_idx = 0 + for waveform, segments in zip(total_wavs, out_segs): + subsegs, subseg_signals = self.subsegment(waveform, + segments, + wav_idx) + total_subsegments.extend(subseg_signals) + total_subsegment_ids.extend(subsegs) + wav_idx += 1 + + inference_response_awaits = [] + for wavs in total_subsegments: + input_tensor_spk0 = pb_utils.Tensor.from_dlpack( + "WAV", to_dlpack(wavs)) + + input_tensors_spk = [input_tensor_spk0] + inference_request = pb_utils.InferenceRequest( + model_name='speaker', + requested_output_names=['EMBEDDINGS'], + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather( + *inference_response_awaits) + + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException( + inference_response.error().message()) + else: + batched_result = pb_utils.get_output_tensor_by_name( + inference_response, 'EMBEDDINGS') + total_embds.extend(from_dlpack(batched_result.to_dlpack())) + + out_embds = list() + out_time_info = list() + for i in range(0, len(total_wavs)): + out_embds.append(list()) + out_time_info.append(list()) + + for subseg_idx, embds in zip(total_subsegment_ids, total_embds): + wav_idx = subseg_idx[0] + out_embds[wav_idx].append(embds) + out_time_info[wav_idx].append(subseg_idx) + + # Begin clustering + inference_response_awaits = [] + for i, embd in enumerate(out_embds): + embd = torch.stack(embd) + input_tensor_embds0 = pb_utils.Tensor.from_dlpack( + "EMBEDDINGS", + to_dlpack(torch.unsqueeze(embd, 0))) + + input_tensors_spk = [input_tensor_embds0] + inference_request = pb_utils.InferenceRequest( + model_name='clusterer', + requested_output_names=['LABELS'], + request_id=str(i), + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather( + *inference_response_awaits) + + i = 0 + results = [] + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException( + inference_response.error().message()) + else: + result = pb_utils.get_output_tensor_by_name( + inference_response, 'LABELS').as_numpy()[0] + utt_to_subseg_labels = self.read_labels(out_time_info[i], + result) + i += 1 + rttm = self.merge_segments(utt_to_subseg_labels) + if len(rttm) > 0: + results.append(rttm) + + # Return the batched resoponse + st = 0 + for b in batch_count: + sents = np.array(results[st:st + b]) + out0 = pb_utils.Tensor("LABELS", sents.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + st += b + return responses diff --git a/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt new file mode 100644 index 00000000..5a51c6ed --- /dev/null +++ b/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "run" +backend: "python" +max_batch_size: 128 + +input [ + { + name: "input" + data_type: TYPE_FP32 + dims: [ -1 ] + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_FP32 + dims: [ -1, 3 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] From ea41934ec7db2132b60019ff7429baa5825308cb Mon Sep 17 00:00:00 2001 From: wd929 Date: Tue, 29 Nov 2022 16:50:42 +0800 Subject: [PATCH 2/6] Update README and copyrights --- runtime/server/diarisation_gpu/README.md | 10 +++------- .../diarisation_gpu/model_repo/clusterer/config.pbtxt | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/runtime/server/diarisation_gpu/README.md b/runtime/server/diarisation_gpu/README.md index 7bdb09cf..8b10c6f3 100644 --- a/runtime/server/diarisation_gpu/README.md +++ b/runtime/server/diarisation_gpu/README.md @@ -31,7 +31,7 @@ Clone the repository: # Clond WeSpeaker repo git clone https://github.com/wenet-e2e/wespeaker.git export WeSpeaker=$PWD/wespeaker/ -cd runtime??? +cd runtime/server/diarisation_gpu export PROJECT_DIR=$PWD ``` @@ -72,8 +72,7 @@ cp external_tools/silero-vad-3.1/files/silero_vad.jit $SAD_DIR/ You can use the following command to access the evluation datas from voxconverse: -bash -``` +```bash bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 ``` @@ -104,9 +103,7 @@ Run the following commands to put the pretrained SAD and Speaker models into cur ```bash cd ${PROJECT_DIR} - mkdir -p model_repo/run/1 - cp -r $SPK_MODEL_REPO/* model_repo/ ``` @@ -171,8 +168,7 @@ Finally, you can get the RTTM information in `$output_directory/rttm`. If you want to test the performances of our SD pipeline, you can run: -bash -``` +```bash perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ -c 0.25 \ -r <(cat data/voxconverse-master/dev/*.rttm) \ diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt index 06b0652e..57effc98 100644 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt @@ -1,4 +1,4 @@ -# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From c0cf9eedfafc719b85ec4b035879fc42951e55e2 Mon Sep 17 00:00:00 2001 From: wd929 Date: Tue, 29 Nov 2022 17:16:15 +0800 Subject: [PATCH 3/6] Code formatted --- runtime/server/diarisation_gpu/README.md | 20 +++---- .../server/diarisation_gpu/client/client.py | 9 ++- .../model_repo/clusterer/config.pbtxt | 2 +- .../diarisation_gpu/model_repo/run/1/model.py | 59 +++++++++---------- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/runtime/server/diarisation_gpu/README.md b/runtime/server/diarisation_gpu/README.md index 8b10c6f3..7e4975b5 100644 --- a/runtime/server/diarisation_gpu/README.md +++ b/runtime/server/diarisation_gpu/README.md @@ -2,7 +2,7 @@ In this best practice, we'll go through how to deploy a WeSpeaker diarisation pipeline in GPU by using NVIDIA [Triton Inference Server](https://github.com/triton-inference-server/server), which contains several modules including SAD, Speaker Embedding Extraction, Clustering and etc. -We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. +We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. ## Table of Contents @@ -21,7 +21,7 @@ We will use [Triton Business Logic Scripting](https://github.com/triton-inferenc ## Preparation -Let's prepare enrivonments, models and data first. +Let's prepare enrivonments, models and data first. ### Prepare Environment @@ -38,7 +38,7 @@ export PROJECT_DIR=$PWD ### Prepare Models -To depoloy this pipeline, first we should obtain SAD and Speaker models. +To depoloy this pipeline, first we should obtain SAD and Speaker models. #### Speaker Models @@ -50,7 +50,7 @@ mkdir -p ${SPK_MODEL_DIR} wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O ${SPK_MODEL_DIR}/voxceleb_resnet34_LM.onnx ``` -Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. +Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. After that, speaker models will be avaliable in `wespeaker/runtime/server/x86_gpu/model_repo/` directory. ```bash @@ -59,7 +59,7 @@ export SPK_MODEL_REPO="wespeaker/runtime/server/x86_gpu/model_repo/" #### SAD Models -Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). +Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). ```bash export SAD_DIR=/workspace/SAD @@ -76,7 +76,7 @@ You can use the following command to access the evluation datas from voxconverse bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 ``` -If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, +If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, ``` abjxc abjxc.wav @@ -128,12 +128,12 @@ Business Logic Scripting (BLS) can execute inference requests on other models be ## Inference Client -In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. +In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. ### Quick Start -Run, +Run, ```bash AUDIO_DATA= @@ -162,11 +162,11 @@ python client.py --url=localhost:8001 --wavscp=$wav_scp_dir/wav.scp --output_dir cat $output_directory/rttm* > $output_directory/rttm ``` -Finally, you can get the RTTM information in `$output_directory/rttm`. +Finally, you can get the RTTM information in `$output_directory/rttm`. ### Compute Metrics -If you want to test the performances of our SD pipeline, you can run: +If you want to test the performances of our SD pipeline, you can run: ```bash perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ diff --git a/runtime/server/diarisation_gpu/client/client.py b/runtime/server/diarisation_gpu/client/client.py index 041bb54d..24723e32 100644 --- a/runtime/server/diarisation_gpu/client/client.py +++ b/runtime/server/diarisation_gpu/client/client.py @@ -113,11 +113,10 @@ def single_job(li): if not os.path.exists(dir_name) and (dir_name != ''): os.makedirs(dir_name) seg_writer = open(os.path.join(FLAGS.output_directory, - 'rttm'+str(idx)), 'w', encoding="utf-8") + 'rttm' + str(idx)), 'w', encoding="utf-8") - with grpcclient.InferenceServerClient( - url=FLAGS.url, - verbose=FLAGS.verbose) as triton_client: + with grpcclient.InferenceServerClient(url=FLAGS.url, + verbose=FLAGS.verbose) as triton_client: protocol_client = grpcclient speech_client = SpeakerClient(triton_client, FLAGS.model_name, protocol_client) @@ -137,7 +136,7 @@ def single_job(li): channel, begin, end - begin, - label)+'\n') + label) + '\n') seg_writer.flush() return predictions diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt index 57effc98..fa671cd4 100644 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt @@ -28,7 +28,7 @@ output [ { name: "LABELS" data_type: TYPE_INT32 - dims: [ -1 ] + dims: [ -1 ] } ] diff --git a/runtime/server/diarisation_gpu/model_repo/run/1/model.py b/runtime/server/diarisation_gpu/model_repo/run/1/model.py index e9a0f2b1..69fe213f 100644 --- a/runtime/server/diarisation_gpu/model_repo/run/1/model.py +++ b/runtime/server/diarisation_gpu/model_repo/run/1/model.py @@ -36,8 +36,8 @@ def initialize(self, args): self.device = "cpu" # Get OUTPUT0 configuration - output0_config = pb_utils.get_output_config_by_name( - model_config, "LABELS") + output0_config = pb_utils.get_output_config_by_name(model_config, + "LABELS") # Convert Triton types to numpy types self.output0_dtype = pb_utils.triton_string_to_numpy( output0_config['data_type']) @@ -62,9 +62,8 @@ def prepare_chunks(self, chunk = wav[current_start_sample: current_start_sample + window_size_samples] if len(chunk) < window_size_samples: - chunk = torch.nn.functional.pad( - chunk, - (0, int(window_size_samples - len(chunk)))) + chunk = torch.nn.functional.pad(chunk, + (0, int(window_size_samples - len(chunk)))) speech_prob = self.sad_model(chunk, 16000) chunks.append(speech_prob) return chunks @@ -119,11 +118,11 @@ def get_timestamps(self, speech_probs, audio_length_samples, speech['start'] = int(max(0, speech['start'] - speech_pad_samples)) if i != len(speeches) - 1: - silence_duration = speeches[i+1]['start'] - speech['end'] + silence_duration = speeches[i + 1]['start'] - speech['end'] if silence_duration < 2 * speech_pad_samples: speech['end'] += int(silence_duration // 2) - speeches[i+1]['start'] = int(max(0, speeches[i+1]['start'] - - silence_duration // 2)) + speeches[i + 1]['start'] = int(max(0, + speeches[i + 1]['start'] - silence_duration // 2)) else: speech['end'] += int(speech_pad_samples) else: @@ -299,14 +298,13 @@ async def execute(self, requests): inference_response_awaits = [] for wavs in total_subsegments: - input_tensor_spk0 = pb_utils.Tensor.from_dlpack( - "WAV", to_dlpack(wavs)) + input_tensor_spk0 = pb_utils.Tensor.from_dlpack("WAV", + to_dlpack(wavs)) input_tensors_spk = [input_tensor_spk0] - inference_request = pb_utils.InferenceRequest( - model_name='speaker', - requested_output_names=['EMBEDDINGS'], - inputs=input_tensors_spk) + inference_request = pb_utils.InferenceRequest(model_name='speaker', + requested_output_names=['EMBEDDINGS'], + inputs=input_tensors_spk) inference_response_awaits.append(inference_request.async_exec()) inference_responses = await asyncio.gather( @@ -314,11 +312,11 @@ async def execute(self, requests): for inference_response in inference_responses: if inference_response.has_error(): - raise pb_utils.TritonModelException( - inference_response.error().message()) + raise pb_utils.TritonModelException(inference_response. + error().message()) else: - batched_result = pb_utils.get_output_tensor_by_name( - inference_response, 'EMBEDDINGS') + batched_result = pb_utils.get_output_tensor_by_name(inference_response, + 'EMBEDDINGS') total_embds.extend(from_dlpack(batched_result.to_dlpack())) out_embds = list() @@ -336,16 +334,14 @@ async def execute(self, requests): inference_response_awaits = [] for i, embd in enumerate(out_embds): embd = torch.stack(embd) - input_tensor_embds0 = pb_utils.Tensor.from_dlpack( - "EMBEDDINGS", - to_dlpack(torch.unsqueeze(embd, 0))) + input_tensor_embds0 = pb_utils.Tensor.from_dlpack("EMBEDDINGS", + to_dlpack(torch.unsqueeze(embd, 0))) input_tensors_spk = [input_tensor_embds0] - inference_request = pb_utils.InferenceRequest( - model_name='clusterer', - requested_output_names=['LABELS'], - request_id=str(i), - inputs=input_tensors_spk) + inference_request = pb_utils.InferenceRequest(model_name='clusterer', + requested_output_names=['LABELS'], + request_id=str(i), + inputs=input_tensors_spk) inference_response_awaits.append(inference_request.async_exec()) inference_responses = await asyncio.gather( @@ -355,11 +351,11 @@ async def execute(self, requests): results = [] for inference_response in inference_responses: if inference_response.has_error(): - raise pb_utils.TritonModelException( - inference_response.error().message()) + raise pb_utils.TritonModelException(inference_response. + error().message()) else: - result = pb_utils.get_output_tensor_by_name( - inference_response, 'LABELS').as_numpy()[0] + result = pb_utils.get_output_tensor_by_name(inference_response, + 'LABELS').as_numpy()[0] utt_to_subseg_labels = self.read_labels(out_time_info[i], result) i += 1 @@ -372,8 +368,7 @@ async def execute(self, requests): for b in batch_count: sents = np.array(results[st:st + b]) out0 = pb_utils.Tensor("LABELS", sents.astype(self.output0_dtype)) - inference_response = pb_utils.InferenceResponse( - output_tensors=[out0]) + inference_response = pb_utils.InferenceResponse(output_tensors=[out0]) responses.append(inference_response) st += b return responses From 748cc7a00bb1f07b45ec983b7b6554ce5d83ffc0 Mon Sep 17 00:00:00 2001 From: wd929 Date: Tue, 29 Nov 2022 17:30:15 +0800 Subject: [PATCH 4/6] Code formatted --- .../model_repo/clusterer/1/model.py | 2 +- .../model_repo/clusterer/config.pbtxt | 2 +- .../diarisation_gpu/model_repo/run/1/model.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py b/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py index 606e05c9..0b879bd3 100644 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py @@ -158,6 +158,6 @@ def execute(self, requests): out0 = pb_utils.Tensor("LABELS", batch_labels.astype(self.output0_dtype)) inference_response = pb_utils.InferenceResponse( - output_tensors=[out0]) + output_tensors=[out0]) responses.append(inference_response) return responses diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt index fa671cd4..87f310cd 100644 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt +++ b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt @@ -28,7 +28,7 @@ output [ { name: "LABELS" data_type: TYPE_INT32 - dims: [ -1 ] + dims: [ -1 ] } ] diff --git a/runtime/server/diarisation_gpu/model_repo/run/1/model.py b/runtime/server/diarisation_gpu/model_repo/run/1/model.py index 69fe213f..805cc591 100644 --- a/runtime/server/diarisation_gpu/model_repo/run/1/model.py +++ b/runtime/server/diarisation_gpu/model_repo/run/1/model.py @@ -36,7 +36,7 @@ def initialize(self, args): self.device = "cpu" # Get OUTPUT0 configuration - output0_config = pb_utils.get_output_config_by_name(model_config, + output0_config = pb_utils.get_output_config_by_name(model_config, "LABELS") # Convert Triton types to numpy types self.output0_dtype = pb_utils.triton_string_to_numpy( @@ -62,8 +62,8 @@ def prepare_chunks(self, chunk = wav[current_start_sample: current_start_sample + window_size_samples] if len(chunk) < window_size_samples: - chunk = torch.nn.functional.pad(chunk, - (0, int(window_size_samples - len(chunk)))) + chunk = torch.nn.functional.pad( + chunk, (0, int(window_size_samples - len(chunk)))) speech_prob = self.sad_model(chunk, 16000) chunks.append(speech_prob) return chunks @@ -121,8 +121,8 @@ def get_timestamps(self, speech_probs, audio_length_samples, silence_duration = speeches[i + 1]['start'] - speech['end'] if silence_duration < 2 * speech_pad_samples: speech['end'] += int(silence_duration // 2) - speeches[i + 1]['start'] = int(max(0, - speeches[i + 1]['start'] - silence_duration // 2)) + speeches[i + 1]['start'] = int( + max(0, speeches[i + 1]['start'] - silence_duration // 2)) else: speech['end'] += int(speech_pad_samples) else: @@ -334,8 +334,8 @@ async def execute(self, requests): inference_response_awaits = [] for i, embd in enumerate(out_embds): embd = torch.stack(embd) - input_tensor_embds0 = pb_utils.Tensor.from_dlpack("EMBEDDINGS", - to_dlpack(torch.unsqueeze(embd, 0))) + input_tensor_embds0 = pb_utils.Tensor.from_dlpack( + "EMBEDDINGS", to_dlpack(torch.unsqueeze(embd, 0))) input_tensors_spk = [input_tensor_embds0] inference_request = pb_utils.InferenceRequest(model_name='clusterer', From 84de57f131ef63be130a33959ae471cfe28c26f4 Mon Sep 17 00:00:00 2001 From: wd929 Date: Wed, 30 Nov 2022 11:11:46 +0800 Subject: [PATCH 5/6] Name unification --- .../Dockerfile/dockerfile.client | 33 ++ .../Dockerfile/dockerfile.server | 38 ++ runtime/server/diarization_gpu/README.md | 179 +++++++++ runtime/server/diarization_gpu/bls.png | Bin 0 -> 54538 bytes .../server/diarization_gpu/client/client.py | 154 ++++++++ .../model_repo/clusterer/1/model.py | 163 ++++++++ .../model_repo/clusterer/config.pbtxt | 43 ++ .../diarization_gpu/model_repo/run/1/model.py | 374 ++++++++++++++++++ .../model_repo/run/config.pbtxt | 43 ++ 9 files changed, 1027 insertions(+) create mode 100644 runtime/server/diarization_gpu/Dockerfile/dockerfile.client create mode 100644 runtime/server/diarization_gpu/Dockerfile/dockerfile.server create mode 100644 runtime/server/diarization_gpu/README.md create mode 100644 runtime/server/diarization_gpu/bls.png create mode 100644 runtime/server/diarization_gpu/client/client.py create mode 100644 runtime/server/diarization_gpu/model_repo/clusterer/1/model.py create mode 100644 runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt create mode 100644 runtime/server/diarization_gpu/model_repo/run/1/model.py create mode 100644 runtime/server/diarization_gpu/model_repo/run/config.pbtxt diff --git a/runtime/server/diarization_gpu/Dockerfile/dockerfile.client b/runtime/server/diarization_gpu/Dockerfile/dockerfile.client new file mode 100644 index 00000000..a7f8219d --- /dev/null +++ b/runtime/server/diarization_gpu/Dockerfile/dockerfile.client @@ -0,0 +1,33 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### + +FROM nvcr.io/nvidia/tritonserver:22.07-py3-sdk +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get install -y libsndfile1 +RUN pip3 install soundfile kaldiio +WORKDIR /workspace diff --git a/runtime/server/diarization_gpu/Dockerfile/dockerfile.server b/runtime/server/diarization_gpu/Dockerfile/dockerfile.server new file mode 100644 index 00000000..510593c6 --- /dev/null +++ b/runtime/server/diarization_gpu/Dockerfile/dockerfile.server @@ -0,0 +1,38 @@ +################################################################################################### +# +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials +# provided with the distribution. +# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used +# to endorse or promote products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +################################################################################################### +FROM nvcr.io/nvidia/tritonserver:22.07-py3 +LABEL maintainer="NVIDIA" +LABEL repository="tritonserver" + +RUN apt-get update && apt-get -y install swig && apt-get -y install python3-dev && apt-get install -y cmake +RUN pip3 install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html +RUN pip3 install -v kaldifeat +RUN python3 -m pip install cupy +RUN python3 -m pip install soundfile +RUN pip install cudf-cu11 dask-cudf-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +RUN pip install cuml-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +RUN pip install cugraph-cu11 --extra-index-url=https://pypi.ngc.nvidia.com +WORKDIR /workspace diff --git a/runtime/server/diarization_gpu/README.md b/runtime/server/diarization_gpu/README.md new file mode 100644 index 00000000..fefd14ff --- /dev/null +++ b/runtime/server/diarization_gpu/README.md @@ -0,0 +1,179 @@ +# Best Practice for Deploying a WeSpeaker diarization service using Triton + +In this best practice, we'll go through how to deploy a WeSpeaker diarization pipeline in GPU by using NVIDIA [Triton Inference Server](https://github.com/triton-inference-server/server), which contains several modules including SAD, Speaker Embedding Extraction, Clustering and etc. + +We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. + +## Table of Contents + +- [Preparation](#preparation) + - [Prepare Environment](#prepare-environment) + - [Prepare Models](#prepare-models) + - [Preapare Test Data](#prepare-test-data) +- [Triton Inference Server](#triton-inference-server) + - [Quick Start](#quick-start) + - [Business Logic Scripting](#bls) +- [Inference Client](#inference-client) + - [Quick Start](#quick-start-1) + - [Compute Metrics](#compute-metrics) +- [Benchmark](#benchmark) + + +## Preparation + +Let's prepare enrivonments, models and data first. + +### Prepare Environment + +Clone the repository: + +```bash +# Clond WeSpeaker repo +git clone https://github.com/wenet-e2e/wespeaker.git +export WeSpeaker=$PWD/wespeaker/ +cd runtime/server/diarization_gpu +export PROJECT_DIR=$PWD + +``` + +### Prepare Models + +To depoloy this pipeline, first we should obtain SAD and Speaker models. + +#### Speaker Models + +You can refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) to train a WeSpeaker model or use a pre-trained model: + +```bash +export SPK_MODEL_DIR=/workspace/pretrained_models +mkdir -p ${SPK_MODEL_DIR} +wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O ${SPK_MODEL_DIR}/voxceleb_resnet34_LM.onnx +``` + +Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. +After that, speaker models will be avaliable in `wespeaker/runtime/server/x86_gpu/model_repo/` directory. + +```bash +export SPK_MODEL_REPO="wespeaker/runtime/server/x86_gpu/model_repo/" +``` + +#### SAD Models + +Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). + +```bash +export SAD_DIR=/workspace/SAD +wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v3.1.zip -O external_tools/silero-vad-v3.1.zip +unzip -o external_tools/silero-vad-v3.1.zip -d external_tools +cp external_tools/silero-vad-3.1/files/silero_vad.jit $SAD_DIR/ +``` + +### Prepare Test Data + +You can use the following command to access the evluation datas from voxconverse: + +```bash +bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 +``` + +If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, + +``` +abjxc abjxc.wav +afjiv afjiv.wav +``` + +## Triton Inference Server + +[Triton Inference Server](https://github.com/triton-inference-server/server) can help with the most of serving work for us and handles requests/results sending and receiving, request scheduling, load balance, and inference execution. In this section, we will use Triton to depoy the diarization pipeline. + +![Pipeline](./bls.png) + +Build the server docker image: +``` +docker build . -f Dockerfile/dockerfile.server -t wespeaker_server:latest --network host +``` + +Build the client docker image: +``` +docker build . -f Dockerfile/dockerfile.client -t wespeaker_client:latest --network host +``` + +Run the following commands to put the pretrained SAD and Speaker models into current `model_repo` directory. + +```bash +cd ${PROJECT_DIR} +mkdir -p model_repo/run/1 +cp -r $SPK_MODEL_REPO/* model_repo/ + +``` + +### Quick Start + +Now start server: + +```bash +# Start the docker container +docker run --gpus all -v $PWD/model_repo:/workspace/model_repo -v $SAD_DIR:/workspace/triton/ --net host --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -it wespeaker_server:latest + +# Inside the docker container +tritonserver --model-repository=/workspace/model_repo + +``` + +### Business Logic Scripting + +Business Logic Scripting (BLS) can execute inference requests on other models being served by Triton as a part of executing one Python model. + + +## Inference Client + +In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. + + +### Quick Start + +Run, + +```bash +AUDIO_DATA= +docker run -ti --net host -v $PWD/client:/ws/client -v $AUDIO_DATA:/ws/test_data wespeaker_client:latest +cd /ws/client +``` + +In the docker container, run the client script to do the whole pipeline inference. + +```bash +# Test one audio +export output_directory="output" +mkdir -p $output_directory +python client.py --url=localhost:8001 --audio_file=/ws/test_data/abjxc.wav --output_directory=$output_directory +cat $output_directory/rttm* > $output_directory/rttm +``` + +The above command sends a single audio `abjxc.wav` to the server and get the result. `--url` option specifies the IP and port of the server, in our example, we set the server and client on the same machine, therefore IP is `localhost`, and we use port `8001` since it is the default port for gRPC in Triton. But if your client is not on the same machine as the server, you should change this option. + +You can also test specify the path of `wav.scp` with `--wavscp` option, then the client will test the audio files in the `wav.scp`. + +```bash +# Test a bunch of audios +export wav_scp_dir=/ws/test_data +python client.py --url=localhost:8001 --wavscp=$wav_scp_dir/wav.scp --output_directory="outp" +cat $output_directory/rttm* > $output_directory/rttm +``` + +Finally, you can get the RTTM information in `$output_directory/rttm`. + +### Compute Metrics + +If you want to test the performances of our SD pipeline, you can run: + +```bash +perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ + -c 0.25 \ + -r <(cat data/voxconverse-master/dev/*.rttm) \ + -s $output_directory/rttm +``` + +## Benchmark (TODO) + diff --git a/runtime/server/diarization_gpu/bls.png b/runtime/server/diarization_gpu/bls.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd15d493acd599c6f2c3ccbed7d0645c92a9238 GIT binary patch literal 54538 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 BvC#kk literal 0 HcmV?d00001 diff --git a/runtime/server/diarization_gpu/client/client.py b/runtime/server/diarization_gpu/client/client.py new file mode 100644 index 00000000..24723e32 --- /dev/null +++ b/runtime/server/diarization_gpu/client/client.py @@ -0,0 +1,154 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +from multiprocessing import Pool + +import tritonclient.grpc as grpcclient +from tritonclient.utils import np_to_triton_dtype +import numpy as np +import soundfile +import argparse +import os + + +class SpeakerClient(object): + def __init__(self, triton_client, model_name, protocol_client): + self.triton_client = triton_client + self.protocol_client = protocol_client + self.model_name = model_name + + def recognize(self, wav_path, client_index): + # We send batchsize=1 data to server + # BatchSize > 1 is also ok but you need to take care of + # padding. + waveform, sample_rate = soundfile.read(wav_path) + cur_length = len(waveform) + input = np.zeros((1, cur_length), dtype=np.float32) + input[0][0:cur_length] = waveform[0:cur_length] + inputs = [self.protocol_client.InferInput("input", input.shape, + np_to_triton_dtype(input.dtype))] + inputs[0].set_data_from_numpy(input) + outputs = [grpcclient.InferRequestedOutput("LABELS")] + response = self.triton_client.infer(self.model_name, + inputs, + request_id=str(client_index), + outputs=outputs) + result = response.as_numpy("LABELS")[0] + return [result] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-v', + '--verbose', + action="store_true", + required=False, + default=False, + help='Enable verbose output') + parser.add_argument('-u', + '--url', + type=str, + required=False, + default='localhost:8001', + help='Inference server URL. Default is ' + 'localhost:8001.') + parser.add_argument('--model_name', + required=False, + default='run', + help='the model to send request to') + parser.add_argument('--wavscp', + type=str, + required=False, + default=None, + help='audio_id \t absolute_wav_path') + parser.add_argument('--output_directory', + type=str, + required=False, + default=None, + help='the path to save the segment files') + parser.add_argument('--data_dir', + type=str, + required=False, + default=None, + help='data dir will be append to audio file if given') + parser.add_argument('--audio_file', + type=str, + required=False, + default=None, + help='single wav file') + FLAGS = parser.parse_args() + + # load data + audio_wavpath = [] + if FLAGS.audio_file is not None: + path = FLAGS.audio_file + if FLAGS.data_dir: + path = os.path.join(FLAGS.data_dir, path) + if os.path.exists(path): + audio_wavpath = [(FLAGS.audio_file, path)] + elif FLAGS.wavscp is not None: + with open(FLAGS.wavscp, "r", encoding="utf-8") as f: + for line in f: + aid, path = line.strip().split(' ') + audio_wavpath.append((aid, path)) + + num_workers = multiprocessing.cpu_count() // 2 + + def single_job(li): + idx, audio_files = li + dir_name = os.path.dirname(FLAGS.output_directory) # get the path + if not os.path.exists(dir_name) and (dir_name != ''): + os.makedirs(dir_name) + seg_writer = open(os.path.join(FLAGS.output_directory, + 'rttm' + str(idx)), 'w', encoding="utf-8") + + with grpcclient.InferenceServerClient(url=FLAGS.url, + verbose=FLAGS.verbose) as triton_client: + protocol_client = grpcclient + speech_client = SpeakerClient(triton_client, FLAGS.model_name, + protocol_client) + + predictions = {} + + for li in audio_files: + utt, wavpath = li + rttms = speech_client.recognize(wavpath, idx)[0] + spec = "SPEAKER {} {} {:.3f} {:.3f} {} " + for i in range(0, rttms.shape[0]): + begin = rttms[i][0] + end = rttms[i][1] + label = int(rttms[i][2]) + channel = 1 + seg_writer.write(spec.format(utt, + channel, + begin, + end - begin, + label) + '\n') + seg_writer.flush() + return predictions + + # start to do inference + # Group requests in batches + predictions = [] + tasks = [] + splits = np.array_split(audio_wavpath, num_workers) + + for idx, per_split in enumerate(splits): + cur_files = per_split.tolist() + tasks.append((idx, cur_files)) + + with Pool(processes=num_workers) as pool: + prediction = pool.map(single_job, tasks) diff --git a/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py b/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py new file mode 100644 index 00000000..0b879bd3 --- /dev/null +++ b/runtime/server/diarization_gpu/model_repo/clusterer/1/model.py @@ -0,0 +1,163 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import from_dlpack +import json +import cupy as cp +import numpy as np +from cuml.cluster import KMeans as cuKM + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + * device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name( + model_config, "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + def cluster_gpu(self, embeddings, p=.01, num_spks=None, + min_num_spks=1, max_num_spks=20): + # Define utility functions + def cosine_similarity(M): + M = M / cp.linalg.norm(M, axis=1, keepdims=True) + return 0.5 * (1.0 + cp.dot(M, M.T)) + + def prune(M, p): + m = M.shape[0] + if m < 1000: + n = max(m - 10, 2) + else: + n = int((1.0 - p) * m) + for i in range(m): + indexes = cp.argsort(M[i, :]) + low_indexes, high_indexes = indexes[0:n], indexes[n:m] + M[i, low_indexes] = 0.0 + M[i, high_indexes] = 1.0 + return 0.5 * (M + M.T) + + def laplacian(M): + M[cp.diag_indices(M.shape[0])] = 0.0 + D = cp.diag(cp.sum(cp.abs(M), axis=1)) + return D - M + + def spectral(M, num_spks, min_num_spks, max_num_spks): + eig_values, eig_vectors = cp.linalg.eigh(M) + num_spks = num_spks if num_spks is not None \ + else cp.argmax(cp.diff(eig_values[:max_num_spks + 1])) + 1 + num_spks = max(num_spks, min_num_spks) + return eig_vectors[:, :num_spks] + + def kmeans(data): + k = data.shape[1] + kmeans_float = cuKM(n_clusters=k, n_init=10, random_state=10) + kmeans_float.fit(cp.asarray(data)) + return kmeans_float.labels_ + + # Fallback for trivial cases + if len(embeddings) <= 2: + return [0] * len(embeddings) + + # Compute similarity matrix + similarity_matrix = cosine_similarity(embeddings) + # Prune matrix with p interval + pruned_similarity_matrix = prune(similarity_matrix, p) + # Compute Laplacian + laplacian_matrix = laplacian(pruned_similarity_matrix) + # Compute spectral embeddings + spectral_embeddings = spectral(laplacian_matrix, num_spks, + min_num_spks, max_num_spks) + # Assign class labels + labels = kmeans(spectral_embeddings) + + return labels + + def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + + Returns + ------- + list + A list of pb_utils.InferenceResponse. + The length of this list must be the same as `requests` + """ + batch_count = [] + total_embd = [] + + responses = [] + for request in requests: + # the requests will all have the same shape + # different shape request will be + # separated by triton inference server + input0 = pb_utils.get_input_tensor_by_name(request, "EMBEDDINGS") + cur_b_embd = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_embd.shape[0] + batch_count.append(cur_batch) + + for embds in cur_b_embd: + total_embd.append(embds.to(self.device)) + + labels_list = [] + for embds in total_embd: + res = self.cluster_gpu(cp.asarray(embds)) + labels_list.append(cp.asnumpy(res)) + + idx = 0 + for b in batch_count: + batch_labels = np.array(labels_list[idx:idx + b]) + idx += b + out0 = pb_utils.Tensor("LABELS", + batch_labels.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse( + output_tensors=[out0]) + responses.append(inference_response) + return responses diff --git a/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt new file mode 100644 index 00000000..87f310cd --- /dev/null +++ b/runtime/server/diarization_gpu/model_repo/clusterer/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "clusterer" +backend: "python" +max_batch_size: 256 + +input [ + { + name: "EMBEDDINGS" + data_type: TYPE_FP32 + dims: [ -1, 256 ] # embedding dim + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_INT32 + dims: [ -1 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] diff --git a/runtime/server/diarization_gpu/model_repo/run/1/model.py b/runtime/server/diarization_gpu/model_repo/run/1/model.py new file mode 100644 index 00000000..805cc591 --- /dev/null +++ b/runtime/server/diarization_gpu/model_repo/run/1/model.py @@ -0,0 +1,374 @@ +import triton_python_backend_utils as pb_utils +from torch.utils.dlpack import to_dlpack, from_dlpack +import torch +import numpy as np +import json +import asyncio + + +class TritonPythonModel: + """Your Python model must use the same class name. Every Python model + that is created must have "TritonPythonModel" as the class name. + """ + + def initialize(self, args): + """`initialize` is called only once when the model is being loaded. + Implementing `initialize` function is optional. This function allows + the model to initialize any state associated with this model. + Parameters + ---------- + args : dict + Both keys and values are strings. The dictionary keys and values are: + * model_config: A JSON string containing the model configuration + * model_instance_kind: A string containing model instance kind + * model_instance_device_id: A string containing model instance + device ID + * model_repository: Model repository path + * model_version: Model version + * model_name: Model name + """ + self.model_config = model_config = json.loads(args['model_config']) + self.max_batch_size = max(model_config["max_batch_size"], 1) + + if "GPU" in model_config["instance_group"][0]["kind"]: + self.device = "cuda" + else: + self.device = "cpu" + + # Get OUTPUT0 configuration + output0_config = pb_utils.get_output_config_by_name(model_config, + "LABELS") + # Convert Triton types to numpy types + self.output0_dtype = pb_utils.triton_string_to_numpy( + output0_config['data_type']) + + self.init_jit_model("/workspace/triton/silero_vad.jit") + + def init_jit_model(self, model_path): + torch.set_grad_enabled(False) + self.sad_model = torch.jit.load(model_path, map_location=self.device) + self.sad_model.eval() + + def prepare_chunks(self, + wav, + audio_length_samples, + sr: int = 16000, + window_size_samples: int = 1536): + chunks = [] + self.sad_model.reset_states() + + for current_start_sample in range(0, audio_length_samples, + window_size_samples): + chunk = wav[current_start_sample: + current_start_sample + window_size_samples] + if len(chunk) < window_size_samples: + chunk = torch.nn.functional.pad( + chunk, (0, int(window_size_samples - len(chunk)))) + speech_prob = self.sad_model(chunk, 16000) + chunks.append(speech_prob) + return chunks + + def get_timestamps(self, speech_probs, audio_length_samples, + sr: int = 16000, + threshold: float = 0.5, + min_duration: float = 0.255, + min_speech_duration_ms: int = 250, + min_silence_duration_ms: int = 100, + window_size_samples: int = 1536, + speech_pad_ms: int = 30): + triggered = False + speeches = [] + current_speech = {} + neg_threshold = threshold - 0.15 + temp_end = 0 + + min_speech_samples = sr * min_speech_duration_ms / 1000 + min_silence_samples = sr * min_silence_duration_ms / 1000 + speech_pad_samples = sr * speech_pad_ms / 1000 + + for i, speech_prob in enumerate(speech_probs): + if (speech_prob >= threshold) and temp_end: + temp_end = 0 + + if (speech_prob >= threshold) and not triggered: + triggered = True + current_speech['start'] = window_size_samples * i + continue + + if (speech_prob < neg_threshold) and triggered: + if not temp_end: + temp_end = window_size_samples * i + if (window_size_samples * i) - temp_end < min_silence_samples: + continue + else: + current_speech['end'] = temp_end + if (current_speech['end'] - + current_speech['start']) > min_speech_samples: + speeches.append(current_speech) + temp_end = 0 + current_speech = {} + triggered = False + continue + if current_speech: + current_speech['end'] = audio_length_samples + speeches.append(current_speech) + + for i, speech in enumerate(speeches): + if i == 0: + speech['start'] = int(max(0, + speech['start'] - speech_pad_samples)) + if i != len(speeches) - 1: + silence_duration = speeches[i + 1]['start'] - speech['end'] + if silence_duration < 2 * speech_pad_samples: + speech['end'] += int(silence_duration // 2) + speeches[i + 1]['start'] = int( + max(0, speeches[i + 1]['start'] - silence_duration // 2)) + else: + speech['end'] += int(speech_pad_samples) + else: + speech['end'] = int(min(audio_length_samples, speech['end'] + + speech_pad_samples)) + vad_result = [] + for item in speeches: + begin = item['start'] / sr + end = item['end'] / sr + if end - begin >= min_duration: + item['start'] = begin + item['end'] = end + vad_result.append(item) + return vad_result + + def subsegment(self, wav, segments, wav_idx, + window_fs: float = 1.50, + period_fs: float = 0.75, + sr: int = 16000, + frame_shift: int = 10): + def repeat_to_fill(x, window_fs): + length = x.size(0) + num = (window_fs + length - 1) // length + + x = x.repeat(1, num)[0][:window_fs] + input = torch.zeros((1, window_fs), device=self.device) + input[0] = x + return input + + subsegs = [] + subseg_signals = [] + + seg_idx = 0 + + window_fs = int(window_fs * sr) + period_fs = int(period_fs * sr) + for segment in segments: + seg_begin, seg_end = int(segment['start'] * sr) + seg_end = int(segment['end'] * sr) + seg_signal = wav[seg_begin: seg_end + 1] + seg_length = seg_end - seg_begin + + if seg_length <= window_fs: + subseg = [wav_idx, seg_idx, + segment['start'], segment['end'], 0, + int(seg_length / sr * 1000 // frame_shift)] + subseg_signal = repeat_to_fill(seg_signal, window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + else: + max_subseg_begin = seg_length - window_fs + period_fs + for subseg_begin in range(0, max_subseg_begin, period_fs): + subseg_end = min(subseg_begin + window_fs, seg_length) + subseg = [wav_idx, seg_idx, + segment['start'], segment['end'], + int(subseg_begin / sr * 1000 / frame_shift), + int(subseg_end / sr * 1000 / frame_shift)] + subseg_signal = repeat_to_fill( + seg_signal[subseg_begin: subseg_end + 1], window_fs) + + subsegs.append(subseg) + subseg_signals.append(subseg_signal) + seg_idx += 1 + + return subsegs, subseg_signals + + def read_labels(self, subseg_ids, label, frame_shift=10): + utt_to_subseg_labels = [] + new_sort = {} + for i, subseg in enumerate(subseg_ids): + (utt, seg_idx, begin_ms, end_ms, begin_frames, end_frames) = subseg + begin = (int(begin_ms) + int(begin_frames) * frame_shift) / 1000.0 + end = (int(begin_ms) + int(end_frames) * frame_shift) / 1000.0 + new_sort[seg_idx] = (begin, end, label[i]) + utt_to_subseg_labels = list(dict(sorted(new_sort.items())).values()) + return utt_to_subseg_labels + + def merge_segments(self, subseg_to_labels): + merged_segment_to_labels = [] + + if len(subseg_to_labels) == 0: + return merged_segment_to_labels + + (begin, end, label) = subseg_to_labels[0] + for (b, e, la) in subseg_to_labels[1:]: + if b <= end and la == label: + end = e + elif b > end: + merged_segment_to_labels.append((begin, end, label)) + begin, end, label = b, e, la + elif b <= end and la != label: + pivot = (b + end) / 2.0 + merged_segment_to_labels.append((begin, pivot, label)) + begin, end, label = pivot, e, la + else: + raise ValueError + merged_segment_to_labels.append((begin, e, label)) + + return merged_segment_to_labels + + async def execute(self, requests): + """`execute` must be implemented in every Python model. `execute` + function receives a list of pb_utils.InferenceRequest as the only + argument. This function is called when an inference is requested + for this model. + Parameters + ---------- + requests : list + A list of pb_utils.InferenceRequest + Returns + ------- + list + A list of pb_utils.InferenceResponse. The length of this list must + be the same as `requests` + """ + + batch_count = [] + batch_len = [] + + total_wavs = [] + total_lens = [] + responses = [] + + for request in requests: + input0 = pb_utils.get_input_tensor_by_name(request, "input") + + cur_b_wav = from_dlpack(input0.to_dlpack()) + cur_batch = cur_b_wav.shape[0] + cur_len = cur_b_wav.shape[1] + batch_count.append(cur_batch) + batch_len.append(cur_len) + + for wav in cur_b_wav: + total_lens.append(len(wav)) + total_wavs.append(wav.to(self.device)) + + speech_shapes = [] + all_probs = [] + + for wav, lens in zip(total_wavs, total_lens): + chunks = self.prepare_chunks(wav, lens) + speech_shapes.append(len(chunks)) + all_probs.append(chunks) + reshape_probs = [] + idx = 0 + for i in range(0, len(speech_shapes)): + cur_speech = [] + for j in range(0, speech_shapes[i]): + cur_speech.append(all_probs[i][j]) + idx += 1 + reshape_probs.append(cur_speech) + + out_segs = [] + for speech_prob, speech_len in zip(reshape_probs, total_lens): + segments = self.get_timestamps(speech_prob, + speech_len, threshold=0.36) + out_segs.append(segments) + + total_subsegments = [] + total_subsegment_ids = [] + total_embds = [] + + wav_idx = 0 + for waveform, segments in zip(total_wavs, out_segs): + subsegs, subseg_signals = self.subsegment(waveform, + segments, + wav_idx) + total_subsegments.extend(subseg_signals) + total_subsegment_ids.extend(subsegs) + wav_idx += 1 + + inference_response_awaits = [] + for wavs in total_subsegments: + input_tensor_spk0 = pb_utils.Tensor.from_dlpack("WAV", + to_dlpack(wavs)) + + input_tensors_spk = [input_tensor_spk0] + inference_request = pb_utils.InferenceRequest(model_name='speaker', + requested_output_names=['EMBEDDINGS'], + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather( + *inference_response_awaits) + + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException(inference_response. + error().message()) + else: + batched_result = pb_utils.get_output_tensor_by_name(inference_response, + 'EMBEDDINGS') + total_embds.extend(from_dlpack(batched_result.to_dlpack())) + + out_embds = list() + out_time_info = list() + for i in range(0, len(total_wavs)): + out_embds.append(list()) + out_time_info.append(list()) + + for subseg_idx, embds in zip(total_subsegment_ids, total_embds): + wav_idx = subseg_idx[0] + out_embds[wav_idx].append(embds) + out_time_info[wav_idx].append(subseg_idx) + + # Begin clustering + inference_response_awaits = [] + for i, embd in enumerate(out_embds): + embd = torch.stack(embd) + input_tensor_embds0 = pb_utils.Tensor.from_dlpack( + "EMBEDDINGS", to_dlpack(torch.unsqueeze(embd, 0))) + + input_tensors_spk = [input_tensor_embds0] + inference_request = pb_utils.InferenceRequest(model_name='clusterer', + requested_output_names=['LABELS'], + request_id=str(i), + inputs=input_tensors_spk) + inference_response_awaits.append(inference_request.async_exec()) + + inference_responses = await asyncio.gather( + *inference_response_awaits) + + i = 0 + results = [] + for inference_response in inference_responses: + if inference_response.has_error(): + raise pb_utils.TritonModelException(inference_response. + error().message()) + else: + result = pb_utils.get_output_tensor_by_name(inference_response, + 'LABELS').as_numpy()[0] + utt_to_subseg_labels = self.read_labels(out_time_info[i], + result) + i += 1 + rttm = self.merge_segments(utt_to_subseg_labels) + if len(rttm) > 0: + results.append(rttm) + + # Return the batched resoponse + st = 0 + for b in batch_count: + sents = np.array(results[st:st + b]) + out0 = pb_utils.Tensor("LABELS", sents.astype(self.output0_dtype)) + inference_response = pb_utils.InferenceResponse(output_tensors=[out0]) + responses.append(inference_response) + st += b + return responses diff --git a/runtime/server/diarization_gpu/model_repo/run/config.pbtxt b/runtime/server/diarization_gpu/model_repo/run/config.pbtxt new file mode 100644 index 00000000..5a51c6ed --- /dev/null +++ b/runtime/server/diarization_gpu/model_repo/run/config.pbtxt @@ -0,0 +1,43 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "run" +backend: "python" +max_batch_size: 128 + +input [ + { + name: "input" + data_type: TYPE_FP32 + dims: [ -1 ] + } +] + +output [ + { + name: "LABELS" + data_type: TYPE_FP32 + dims: [ -1, 3 ] + } +] + +dynamic_batching { + preferred_batch_size: [ 16, 32 ] + } +instance_group [ + { + count: 2 + kind: KIND_GPU + } +] From 0dd9146ecc311ba5d1121aeebf2be0ea947faa9e Mon Sep 17 00:00:00 2001 From: wd929 Date: Wed, 30 Nov 2022 11:12:44 +0800 Subject: [PATCH 6/6] Name unification --- .../Dockerfile/dockerfile.client | 33 -- .../Dockerfile/dockerfile.server | 38 -- runtime/server/diarisation_gpu/README.md | 179 --------- runtime/server/diarisation_gpu/bls.png | Bin 54538 -> 0 bytes .../server/diarisation_gpu/client/client.py | 154 -------- .../model_repo/clusterer/1/model.py | 163 -------- .../model_repo/clusterer/config.pbtxt | 43 -- .../diarisation_gpu/model_repo/run/1/model.py | 374 ------------------ .../model_repo/run/config.pbtxt | 43 -- 9 files changed, 1027 deletions(-) delete mode 100644 runtime/server/diarisation_gpu/Dockerfile/dockerfile.client delete mode 100644 runtime/server/diarisation_gpu/Dockerfile/dockerfile.server delete mode 100644 runtime/server/diarisation_gpu/README.md delete mode 100644 runtime/server/diarisation_gpu/bls.png delete mode 100644 runtime/server/diarisation_gpu/client/client.py delete mode 100644 runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py delete mode 100644 runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt delete mode 100644 runtime/server/diarisation_gpu/model_repo/run/1/model.py delete mode 100644 runtime/server/diarisation_gpu/model_repo/run/config.pbtxt diff --git a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client deleted file mode 100644 index a7f8219d..00000000 --- a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.client +++ /dev/null @@ -1,33 +0,0 @@ -################################################################################################### -# -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are permitted -# provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright notice, this list of -# conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, this list of -# conditions and the following disclaimer in the documentation and/or other materials -# provided with the distribution. -# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used -# to endorse or promote products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -################################################################################################### - -FROM nvcr.io/nvidia/tritonserver:22.07-py3-sdk -LABEL maintainer="NVIDIA" -LABEL repository="tritonserver" - -RUN apt-get update && apt-get install -y libsndfile1 -RUN pip3 install soundfile kaldiio -WORKDIR /workspace diff --git a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server b/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server deleted file mode 100644 index 510593c6..00000000 --- a/runtime/server/diarisation_gpu/Dockerfile/dockerfile.server +++ /dev/null @@ -1,38 +0,0 @@ -################################################################################################### -# -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are permitted -# provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright notice, this list of -# conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, this list of -# conditions and the following disclaimer in the documentation and/or other materials -# provided with the distribution. -# * Neither the name of the NVIDIA CORPORATION nor the names of its contributors may be used -# to endorse or promote products derived from this software without specific prior written -# permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NVIDIA CORPORATION BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -# STRICT LIABILITY, OR TOR (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -################################################################################################### -FROM nvcr.io/nvidia/tritonserver:22.07-py3 -LABEL maintainer="NVIDIA" -LABEL repository="tritonserver" - -RUN apt-get update && apt-get -y install swig && apt-get -y install python3-dev && apt-get install -y cmake -RUN pip3 install torch==1.10.0+cu113 torchvision==0.11.1+cu113 torchaudio==0.10.0+cu113 -f https://download.pytorch.org/whl/cu113/torch_stable.html -RUN pip3 install -v kaldifeat -RUN python3 -m pip install cupy -RUN python3 -m pip install soundfile -RUN pip install cudf-cu11 dask-cudf-cu11 --extra-index-url=https://pypi.ngc.nvidia.com -RUN pip install cuml-cu11 --extra-index-url=https://pypi.ngc.nvidia.com -RUN pip install cugraph-cu11 --extra-index-url=https://pypi.ngc.nvidia.com -WORKDIR /workspace diff --git a/runtime/server/diarisation_gpu/README.md b/runtime/server/diarisation_gpu/README.md deleted file mode 100644 index 7e4975b5..00000000 --- a/runtime/server/diarisation_gpu/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# Best Practice for Deploying a WeSpeaker diarisation service using Triton - -In this best practice, we'll go through how to deploy a WeSpeaker diarisation pipeline in GPU by using NVIDIA [Triton Inference Server](https://github.com/triton-inference-server/server), which contains several modules including SAD, Speaker Embedding Extraction, Clustering and etc. - -We will use [Triton Business Logic Scripting](https://github.com/triton-inference-server/python_backend#business-logic-scripting) (BLS) to implement this pipeline. - -## Table of Contents - -- [Preparation](#preparation) - - [Prepare Environment](#prepare-environment) - - [Prepare Models](#prepare-models) - - [Preapare Test Data](#prepare-test-data) -- [Triton Inference Server](#triton-inference-server) - - [Quick Start](#quick-start) - - [Business Logic Scripting](#bls) -- [Inference Client](#inference-client) - - [Quick Start](#quick-start-1) - - [Compute Metrics](#compute-metrics) -- [Benchmark](#benchmark) - - -## Preparation - -Let's prepare enrivonments, models and data first. - -### Prepare Environment - -Clone the repository: - -```bash -# Clond WeSpeaker repo -git clone https://github.com/wenet-e2e/wespeaker.git -export WeSpeaker=$PWD/wespeaker/ -cd runtime/server/diarisation_gpu -export PROJECT_DIR=$PWD - -``` - -### Prepare Models - -To depoloy this pipeline, first we should obtain SAD and Speaker models. - -#### Speaker Models - -You can refer to [voxceleb sv recipe](https://github.com/wenet-e2e/wespeaker/tree/master/examples/voxceleb/v2) to train a WeSpeaker model or use a pre-trained model: - -```bash -export SPK_MODEL_DIR=/workspace/pretrained_models -mkdir -p ${SPK_MODEL_DIR} -wget -c https://wespeaker-1256283475.cos.ap-shanghai.myqcloud.com/models/voxceleb/voxceleb_resnet34_LM.onnx -O ${SPK_MODEL_DIR}/voxceleb_resnet34_LM.onnx -``` - -Then you can follow the best practice of [GPU deployment](https://github.com/wenet-e2e/wespeaker/tree/master/runtime/server/x86_gpu) to deploy the WeSpeaker model in Triton. -After that, speaker models will be avaliable in `wespeaker/runtime/server/x86_gpu/model_repo/` directory. - -```bash -export SPK_MODEL_REPO="wespeaker/runtime/server/x86_gpu/model_repo/" -``` - -#### SAD Models - -Speaker activity detection model: system SAD (VAD model pretrained by [silero](https://github.com/snakers4/silero-vad)). - -```bash -export SAD_DIR=/workspace/SAD -wget -c https://github.com/snakers4/silero-vad/archive/refs/tags/v3.1.zip -O external_tools/silero-vad-v3.1.zip -unzip -o external_tools/silero-vad-v3.1.zip -d external_tools -cp external_tools/silero-vad-3.1/files/silero_vad.jit $SAD_DIR/ -``` - -### Prepare Test Data - -You can use the following command to access the evluation datas from voxconverse: - -```bash -bash $WeSpeaker/examples/voxconverse/v1/run.sh --stage 2 --stop_stage 2 -``` - -If you are using your own data, you can evaluate the audio one by one. Or you should preapre a `wav.scp`, which contains a list of audios. For example, - -``` -abjxc abjxc.wav -afjiv afjiv.wav -``` - -## Triton Inference Server - -[Triton Inference Server](https://github.com/triton-inference-server/server) can help with the most of serving work for us and handles requests/results sending and receiving, request scheduling, load balance, and inference execution. In this section, we will use Triton to depoy the diarisation pipeline. - -![Pipeline](./bls.png) - -Build the server docker image: -``` -docker build . -f Dockerfile/dockerfile.server -t wespeaker_server:latest --network host -``` - -Build the client docker image: -``` -docker build . -f Dockerfile/dockerfile.client -t wespeaker_client:latest --network host -``` - -Run the following commands to put the pretrained SAD and Speaker models into current `model_repo` directory. - -```bash -cd ${PROJECT_DIR} -mkdir -p model_repo/run/1 -cp -r $SPK_MODEL_REPO/* model_repo/ - -``` - -### Quick Start - -Now start server: - -```bash -# Start the docker container -docker run --gpus all -v $PWD/model_repo:/workspace/model_repo -v $SAD_DIR:/workspace/triton/ --net host --shm-size=1g --ulimit memlock=-1 -p 8000:8000 -p 8001:8001 -p 8002:8002 --ulimit stack=67108864 -it wespeaker_server:latest - -# Inside the docker container -tritonserver --model-repository=/workspace/model_repo - -``` - -### Business Logic Scripting - -Business Logic Scripting (BLS) can execute inference requests on other models being served by Triton as a part of executing one Python model. - - -## Inference Client - -In this section, we will show how to send requests to our deployed SD service, and receive the RTTM results. - - -### Quick Start - -Run, - -```bash -AUDIO_DATA= -docker run -ti --net host -v $PWD/client:/ws/client -v $AUDIO_DATA:/ws/test_data wespeaker_client:latest -cd /ws/client -``` - -In the docker container, run the client script to do the whole pipeline inference. - -```bash -# Test one audio -export output_directory="output" -mkdir -p $output_directory -python client.py --url=localhost:8001 --audio_file=/ws/test_data/abjxc.wav --output_directory=$output_directory -cat $output_directory/rttm* > $output_directory/rttm -``` - -The above command sends a single audio `abjxc.wav` to the server and get the result. `--url` option specifies the IP and port of the server, in our example, we set the server and client on the same machine, therefore IP is `localhost`, and we use port `8001` since it is the default port for gRPC in Triton. But if your client is not on the same machine as the server, you should change this option. - -You can also test specify the path of `wav.scp` with `--wavscp` option, then the client will test the audio files in the `wav.scp`. - -```bash -# Test a bunch of audios -export wav_scp_dir=/ws/test_data -python client.py --url=localhost:8001 --wavscp=$wav_scp_dir/wav.scp --output_directory="outp" -cat $output_directory/rttm* > $output_directory/rttm -``` - -Finally, you can get the RTTM information in `$output_directory/rttm`. - -### Compute Metrics - -If you want to test the performances of our SD pipeline, you can run: - -```bash -perl external_tools/SCTK-2.4.12/src/md-eval/md-eval.pl \ - -c 0.25 \ - -r <(cat data/voxconverse-master/dev/*.rttm) \ - -s $output_directory/rttm -``` - -## Benchmark (TODO) - diff --git a/runtime/server/diarisation_gpu/bls.png b/runtime/server/diarisation_gpu/bls.png deleted file mode 100644 index 3dd15d493acd599c6f2c3ccbed7d0645c92a9238..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54538 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 BvC#kk diff --git a/runtime/server/diarisation_gpu/client/client.py b/runtime/server/diarisation_gpu/client/client.py deleted file mode 100644 index 24723e32..00000000 --- a/runtime/server/diarisation_gpu/client/client.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import multiprocessing -from multiprocessing import Pool - -import tritonclient.grpc as grpcclient -from tritonclient.utils import np_to_triton_dtype -import numpy as np -import soundfile -import argparse -import os - - -class SpeakerClient(object): - def __init__(self, triton_client, model_name, protocol_client): - self.triton_client = triton_client - self.protocol_client = protocol_client - self.model_name = model_name - - def recognize(self, wav_path, client_index): - # We send batchsize=1 data to server - # BatchSize > 1 is also ok but you need to take care of - # padding. - waveform, sample_rate = soundfile.read(wav_path) - cur_length = len(waveform) - input = np.zeros((1, cur_length), dtype=np.float32) - input[0][0:cur_length] = waveform[0:cur_length] - inputs = [self.protocol_client.InferInput("input", input.shape, - np_to_triton_dtype(input.dtype))] - inputs[0].set_data_from_numpy(input) - outputs = [grpcclient.InferRequestedOutput("LABELS")] - response = self.triton_client.infer(self.model_name, - inputs, - request_id=str(client_index), - outputs=outputs) - result = response.as_numpy("LABELS")[0] - return [result] - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-v', - '--verbose', - action="store_true", - required=False, - default=False, - help='Enable verbose output') - parser.add_argument('-u', - '--url', - type=str, - required=False, - default='localhost:8001', - help='Inference server URL. Default is ' - 'localhost:8001.') - parser.add_argument('--model_name', - required=False, - default='run', - help='the model to send request to') - parser.add_argument('--wavscp', - type=str, - required=False, - default=None, - help='audio_id \t absolute_wav_path') - parser.add_argument('--output_directory', - type=str, - required=False, - default=None, - help='the path to save the segment files') - parser.add_argument('--data_dir', - type=str, - required=False, - default=None, - help='data dir will be append to audio file if given') - parser.add_argument('--audio_file', - type=str, - required=False, - default=None, - help='single wav file') - FLAGS = parser.parse_args() - - # load data - audio_wavpath = [] - if FLAGS.audio_file is not None: - path = FLAGS.audio_file - if FLAGS.data_dir: - path = os.path.join(FLAGS.data_dir, path) - if os.path.exists(path): - audio_wavpath = [(FLAGS.audio_file, path)] - elif FLAGS.wavscp is not None: - with open(FLAGS.wavscp, "r", encoding="utf-8") as f: - for line in f: - aid, path = line.strip().split(' ') - audio_wavpath.append((aid, path)) - - num_workers = multiprocessing.cpu_count() // 2 - - def single_job(li): - idx, audio_files = li - dir_name = os.path.dirname(FLAGS.output_directory) # get the path - if not os.path.exists(dir_name) and (dir_name != ''): - os.makedirs(dir_name) - seg_writer = open(os.path.join(FLAGS.output_directory, - 'rttm' + str(idx)), 'w', encoding="utf-8") - - with grpcclient.InferenceServerClient(url=FLAGS.url, - verbose=FLAGS.verbose) as triton_client: - protocol_client = grpcclient - speech_client = SpeakerClient(triton_client, FLAGS.model_name, - protocol_client) - - predictions = {} - - for li in audio_files: - utt, wavpath = li - rttms = speech_client.recognize(wavpath, idx)[0] - spec = "SPEAKER {} {} {:.3f} {:.3f} {} " - for i in range(0, rttms.shape[0]): - begin = rttms[i][0] - end = rttms[i][1] - label = int(rttms[i][2]) - channel = 1 - seg_writer.write(spec.format(utt, - channel, - begin, - end - begin, - label) + '\n') - seg_writer.flush() - return predictions - - # start to do inference - # Group requests in batches - predictions = [] - tasks = [] - splits = np.array_split(audio_wavpath, num_workers) - - for idx, per_split in enumerate(splits): - cur_files = per_split.tolist() - tasks.append((idx, cur_files)) - - with Pool(processes=num_workers) as pool: - prediction = pool.map(single_job, tasks) diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py b/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py deleted file mode 100644 index 0b879bd3..00000000 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/1/model.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import triton_python_backend_utils as pb_utils -from torch.utils.dlpack import from_dlpack -import json -import cupy as cp -import numpy as np -from cuml.cluster import KMeans as cuKM - - -class TritonPythonModel: - """Your Python model must use the same class name. Every Python model - that is created must have "TritonPythonModel" as the class name. - """ - - def initialize(self, args): - """`initialize` is called only once when the model is being loaded. - Implementing `initialize` function is optional. This function allows - the model to initialize any state associated with this model. - - Parameters - ---------- - args : dict - Both keys and values are strings. The dictionary keys and values are: - * model_config: A JSON string containing the model configuration - * model_instance_kind: A string containing model instance kind - * model_instance_device_id: A string containing model instance - * device ID - * model_repository: Model repository path - * model_version: Model version - * model_name: Model name - """ - self.model_config = model_config = json.loads(args['model_config']) - self.max_batch_size = max(model_config["max_batch_size"], 1) - - if "GPU" in model_config["instance_group"][0]["kind"]: - self.device = "cuda" - else: - self.device = "cpu" - - # Get OUTPUT0 configuration - output0_config = pb_utils.get_output_config_by_name( - model_config, "LABELS") - # Convert Triton types to numpy types - self.output0_dtype = pb_utils.triton_string_to_numpy( - output0_config['data_type']) - - def cluster_gpu(self, embeddings, p=.01, num_spks=None, - min_num_spks=1, max_num_spks=20): - # Define utility functions - def cosine_similarity(M): - M = M / cp.linalg.norm(M, axis=1, keepdims=True) - return 0.5 * (1.0 + cp.dot(M, M.T)) - - def prune(M, p): - m = M.shape[0] - if m < 1000: - n = max(m - 10, 2) - else: - n = int((1.0 - p) * m) - for i in range(m): - indexes = cp.argsort(M[i, :]) - low_indexes, high_indexes = indexes[0:n], indexes[n:m] - M[i, low_indexes] = 0.0 - M[i, high_indexes] = 1.0 - return 0.5 * (M + M.T) - - def laplacian(M): - M[cp.diag_indices(M.shape[0])] = 0.0 - D = cp.diag(cp.sum(cp.abs(M), axis=1)) - return D - M - - def spectral(M, num_spks, min_num_spks, max_num_spks): - eig_values, eig_vectors = cp.linalg.eigh(M) - num_spks = num_spks if num_spks is not None \ - else cp.argmax(cp.diff(eig_values[:max_num_spks + 1])) + 1 - num_spks = max(num_spks, min_num_spks) - return eig_vectors[:, :num_spks] - - def kmeans(data): - k = data.shape[1] - kmeans_float = cuKM(n_clusters=k, n_init=10, random_state=10) - kmeans_float.fit(cp.asarray(data)) - return kmeans_float.labels_ - - # Fallback for trivial cases - if len(embeddings) <= 2: - return [0] * len(embeddings) - - # Compute similarity matrix - similarity_matrix = cosine_similarity(embeddings) - # Prune matrix with p interval - pruned_similarity_matrix = prune(similarity_matrix, p) - # Compute Laplacian - laplacian_matrix = laplacian(pruned_similarity_matrix) - # Compute spectral embeddings - spectral_embeddings = spectral(laplacian_matrix, num_spks, - min_num_spks, max_num_spks) - # Assign class labels - labels = kmeans(spectral_embeddings) - - return labels - - def execute(self, requests): - """`execute` must be implemented in every Python model. `execute` - function receives a list of pb_utils.InferenceRequest as the only - argument. This function is called when an inference is requested - for this model. - - Parameters - ---------- - requests : list - A list of pb_utils.InferenceRequest - - Returns - ------- - list - A list of pb_utils.InferenceResponse. - The length of this list must be the same as `requests` - """ - batch_count = [] - total_embd = [] - - responses = [] - for request in requests: - # the requests will all have the same shape - # different shape request will be - # separated by triton inference server - input0 = pb_utils.get_input_tensor_by_name(request, "EMBEDDINGS") - cur_b_embd = from_dlpack(input0.to_dlpack()) - cur_batch = cur_b_embd.shape[0] - batch_count.append(cur_batch) - - for embds in cur_b_embd: - total_embd.append(embds.to(self.device)) - - labels_list = [] - for embds in total_embd: - res = self.cluster_gpu(cp.asarray(embds)) - labels_list.append(cp.asnumpy(res)) - - idx = 0 - for b in batch_count: - batch_labels = np.array(labels_list[idx:idx + b]) - idx += b - out0 = pb_utils.Tensor("LABELS", - batch_labels.astype(self.output0_dtype)) - inference_response = pb_utils.InferenceResponse( - output_tensors=[out0]) - responses.append(inference_response) - return responses diff --git a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt deleted file mode 100644 index 87f310cd..00000000 --- a/runtime/server/diarisation_gpu/model_repo/clusterer/config.pbtxt +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "clusterer" -backend: "python" -max_batch_size: 256 - -input [ - { - name: "EMBEDDINGS" - data_type: TYPE_FP32 - dims: [ -1, 256 ] # embedding dim - } -] - -output [ - { - name: "LABELS" - data_type: TYPE_INT32 - dims: [ -1 ] - } -] - -dynamic_batching { - preferred_batch_size: [ 16, 32 ] - } -instance_group [ - { - count: 2 - kind: KIND_GPU - } -] diff --git a/runtime/server/diarisation_gpu/model_repo/run/1/model.py b/runtime/server/diarisation_gpu/model_repo/run/1/model.py deleted file mode 100644 index 805cc591..00000000 --- a/runtime/server/diarisation_gpu/model_repo/run/1/model.py +++ /dev/null @@ -1,374 +0,0 @@ -import triton_python_backend_utils as pb_utils -from torch.utils.dlpack import to_dlpack, from_dlpack -import torch -import numpy as np -import json -import asyncio - - -class TritonPythonModel: - """Your Python model must use the same class name. Every Python model - that is created must have "TritonPythonModel" as the class name. - """ - - def initialize(self, args): - """`initialize` is called only once when the model is being loaded. - Implementing `initialize` function is optional. This function allows - the model to initialize any state associated with this model. - Parameters - ---------- - args : dict - Both keys and values are strings. The dictionary keys and values are: - * model_config: A JSON string containing the model configuration - * model_instance_kind: A string containing model instance kind - * model_instance_device_id: A string containing model instance - device ID - * model_repository: Model repository path - * model_version: Model version - * model_name: Model name - """ - self.model_config = model_config = json.loads(args['model_config']) - self.max_batch_size = max(model_config["max_batch_size"], 1) - - if "GPU" in model_config["instance_group"][0]["kind"]: - self.device = "cuda" - else: - self.device = "cpu" - - # Get OUTPUT0 configuration - output0_config = pb_utils.get_output_config_by_name(model_config, - "LABELS") - # Convert Triton types to numpy types - self.output0_dtype = pb_utils.triton_string_to_numpy( - output0_config['data_type']) - - self.init_jit_model("/workspace/triton/silero_vad.jit") - - def init_jit_model(self, model_path): - torch.set_grad_enabled(False) - self.sad_model = torch.jit.load(model_path, map_location=self.device) - self.sad_model.eval() - - def prepare_chunks(self, - wav, - audio_length_samples, - sr: int = 16000, - window_size_samples: int = 1536): - chunks = [] - self.sad_model.reset_states() - - for current_start_sample in range(0, audio_length_samples, - window_size_samples): - chunk = wav[current_start_sample: - current_start_sample + window_size_samples] - if len(chunk) < window_size_samples: - chunk = torch.nn.functional.pad( - chunk, (0, int(window_size_samples - len(chunk)))) - speech_prob = self.sad_model(chunk, 16000) - chunks.append(speech_prob) - return chunks - - def get_timestamps(self, speech_probs, audio_length_samples, - sr: int = 16000, - threshold: float = 0.5, - min_duration: float = 0.255, - min_speech_duration_ms: int = 250, - min_silence_duration_ms: int = 100, - window_size_samples: int = 1536, - speech_pad_ms: int = 30): - triggered = False - speeches = [] - current_speech = {} - neg_threshold = threshold - 0.15 - temp_end = 0 - - min_speech_samples = sr * min_speech_duration_ms / 1000 - min_silence_samples = sr * min_silence_duration_ms / 1000 - speech_pad_samples = sr * speech_pad_ms / 1000 - - for i, speech_prob in enumerate(speech_probs): - if (speech_prob >= threshold) and temp_end: - temp_end = 0 - - if (speech_prob >= threshold) and not triggered: - triggered = True - current_speech['start'] = window_size_samples * i - continue - - if (speech_prob < neg_threshold) and triggered: - if not temp_end: - temp_end = window_size_samples * i - if (window_size_samples * i) - temp_end < min_silence_samples: - continue - else: - current_speech['end'] = temp_end - if (current_speech['end'] - - current_speech['start']) > min_speech_samples: - speeches.append(current_speech) - temp_end = 0 - current_speech = {} - triggered = False - continue - if current_speech: - current_speech['end'] = audio_length_samples - speeches.append(current_speech) - - for i, speech in enumerate(speeches): - if i == 0: - speech['start'] = int(max(0, - speech['start'] - speech_pad_samples)) - if i != len(speeches) - 1: - silence_duration = speeches[i + 1]['start'] - speech['end'] - if silence_duration < 2 * speech_pad_samples: - speech['end'] += int(silence_duration // 2) - speeches[i + 1]['start'] = int( - max(0, speeches[i + 1]['start'] - silence_duration // 2)) - else: - speech['end'] += int(speech_pad_samples) - else: - speech['end'] = int(min(audio_length_samples, speech['end'] - + speech_pad_samples)) - vad_result = [] - for item in speeches: - begin = item['start'] / sr - end = item['end'] / sr - if end - begin >= min_duration: - item['start'] = begin - item['end'] = end - vad_result.append(item) - return vad_result - - def subsegment(self, wav, segments, wav_idx, - window_fs: float = 1.50, - period_fs: float = 0.75, - sr: int = 16000, - frame_shift: int = 10): - def repeat_to_fill(x, window_fs): - length = x.size(0) - num = (window_fs + length - 1) // length - - x = x.repeat(1, num)[0][:window_fs] - input = torch.zeros((1, window_fs), device=self.device) - input[0] = x - return input - - subsegs = [] - subseg_signals = [] - - seg_idx = 0 - - window_fs = int(window_fs * sr) - period_fs = int(period_fs * sr) - for segment in segments: - seg_begin, seg_end = int(segment['start'] * sr) - seg_end = int(segment['end'] * sr) - seg_signal = wav[seg_begin: seg_end + 1] - seg_length = seg_end - seg_begin - - if seg_length <= window_fs: - subseg = [wav_idx, seg_idx, - segment['start'], segment['end'], 0, - int(seg_length / sr * 1000 // frame_shift)] - subseg_signal = repeat_to_fill(seg_signal, window_fs) - - subsegs.append(subseg) - subseg_signals.append(subseg_signal) - seg_idx += 1 - else: - max_subseg_begin = seg_length - window_fs + period_fs - for subseg_begin in range(0, max_subseg_begin, period_fs): - subseg_end = min(subseg_begin + window_fs, seg_length) - subseg = [wav_idx, seg_idx, - segment['start'], segment['end'], - int(subseg_begin / sr * 1000 / frame_shift), - int(subseg_end / sr * 1000 / frame_shift)] - subseg_signal = repeat_to_fill( - seg_signal[subseg_begin: subseg_end + 1], window_fs) - - subsegs.append(subseg) - subseg_signals.append(subseg_signal) - seg_idx += 1 - - return subsegs, subseg_signals - - def read_labels(self, subseg_ids, label, frame_shift=10): - utt_to_subseg_labels = [] - new_sort = {} - for i, subseg in enumerate(subseg_ids): - (utt, seg_idx, begin_ms, end_ms, begin_frames, end_frames) = subseg - begin = (int(begin_ms) + int(begin_frames) * frame_shift) / 1000.0 - end = (int(begin_ms) + int(end_frames) * frame_shift) / 1000.0 - new_sort[seg_idx] = (begin, end, label[i]) - utt_to_subseg_labels = list(dict(sorted(new_sort.items())).values()) - return utt_to_subseg_labels - - def merge_segments(self, subseg_to_labels): - merged_segment_to_labels = [] - - if len(subseg_to_labels) == 0: - return merged_segment_to_labels - - (begin, end, label) = subseg_to_labels[0] - for (b, e, la) in subseg_to_labels[1:]: - if b <= end and la == label: - end = e - elif b > end: - merged_segment_to_labels.append((begin, end, label)) - begin, end, label = b, e, la - elif b <= end and la != label: - pivot = (b + end) / 2.0 - merged_segment_to_labels.append((begin, pivot, label)) - begin, end, label = pivot, e, la - else: - raise ValueError - merged_segment_to_labels.append((begin, e, label)) - - return merged_segment_to_labels - - async def execute(self, requests): - """`execute` must be implemented in every Python model. `execute` - function receives a list of pb_utils.InferenceRequest as the only - argument. This function is called when an inference is requested - for this model. - Parameters - ---------- - requests : list - A list of pb_utils.InferenceRequest - Returns - ------- - list - A list of pb_utils.InferenceResponse. The length of this list must - be the same as `requests` - """ - - batch_count = [] - batch_len = [] - - total_wavs = [] - total_lens = [] - responses = [] - - for request in requests: - input0 = pb_utils.get_input_tensor_by_name(request, "input") - - cur_b_wav = from_dlpack(input0.to_dlpack()) - cur_batch = cur_b_wav.shape[0] - cur_len = cur_b_wav.shape[1] - batch_count.append(cur_batch) - batch_len.append(cur_len) - - for wav in cur_b_wav: - total_lens.append(len(wav)) - total_wavs.append(wav.to(self.device)) - - speech_shapes = [] - all_probs = [] - - for wav, lens in zip(total_wavs, total_lens): - chunks = self.prepare_chunks(wav, lens) - speech_shapes.append(len(chunks)) - all_probs.append(chunks) - reshape_probs = [] - idx = 0 - for i in range(0, len(speech_shapes)): - cur_speech = [] - for j in range(0, speech_shapes[i]): - cur_speech.append(all_probs[i][j]) - idx += 1 - reshape_probs.append(cur_speech) - - out_segs = [] - for speech_prob, speech_len in zip(reshape_probs, total_lens): - segments = self.get_timestamps(speech_prob, - speech_len, threshold=0.36) - out_segs.append(segments) - - total_subsegments = [] - total_subsegment_ids = [] - total_embds = [] - - wav_idx = 0 - for waveform, segments in zip(total_wavs, out_segs): - subsegs, subseg_signals = self.subsegment(waveform, - segments, - wav_idx) - total_subsegments.extend(subseg_signals) - total_subsegment_ids.extend(subsegs) - wav_idx += 1 - - inference_response_awaits = [] - for wavs in total_subsegments: - input_tensor_spk0 = pb_utils.Tensor.from_dlpack("WAV", - to_dlpack(wavs)) - - input_tensors_spk = [input_tensor_spk0] - inference_request = pb_utils.InferenceRequest(model_name='speaker', - requested_output_names=['EMBEDDINGS'], - inputs=input_tensors_spk) - inference_response_awaits.append(inference_request.async_exec()) - - inference_responses = await asyncio.gather( - *inference_response_awaits) - - for inference_response in inference_responses: - if inference_response.has_error(): - raise pb_utils.TritonModelException(inference_response. - error().message()) - else: - batched_result = pb_utils.get_output_tensor_by_name(inference_response, - 'EMBEDDINGS') - total_embds.extend(from_dlpack(batched_result.to_dlpack())) - - out_embds = list() - out_time_info = list() - for i in range(0, len(total_wavs)): - out_embds.append(list()) - out_time_info.append(list()) - - for subseg_idx, embds in zip(total_subsegment_ids, total_embds): - wav_idx = subseg_idx[0] - out_embds[wav_idx].append(embds) - out_time_info[wav_idx].append(subseg_idx) - - # Begin clustering - inference_response_awaits = [] - for i, embd in enumerate(out_embds): - embd = torch.stack(embd) - input_tensor_embds0 = pb_utils.Tensor.from_dlpack( - "EMBEDDINGS", to_dlpack(torch.unsqueeze(embd, 0))) - - input_tensors_spk = [input_tensor_embds0] - inference_request = pb_utils.InferenceRequest(model_name='clusterer', - requested_output_names=['LABELS'], - request_id=str(i), - inputs=input_tensors_spk) - inference_response_awaits.append(inference_request.async_exec()) - - inference_responses = await asyncio.gather( - *inference_response_awaits) - - i = 0 - results = [] - for inference_response in inference_responses: - if inference_response.has_error(): - raise pb_utils.TritonModelException(inference_response. - error().message()) - else: - result = pb_utils.get_output_tensor_by_name(inference_response, - 'LABELS').as_numpy()[0] - utt_to_subseg_labels = self.read_labels(out_time_info[i], - result) - i += 1 - rttm = self.merge_segments(utt_to_subseg_labels) - if len(rttm) > 0: - results.append(rttm) - - # Return the batched resoponse - st = 0 - for b in batch_count: - sents = np.array(results[st:st + b]) - out0 = pb_utils.Tensor("LABELS", sents.astype(self.output0_dtype)) - inference_response = pb_utils.InferenceResponse(output_tensors=[out0]) - responses.append(inference_response) - st += b - return responses diff --git a/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt b/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt deleted file mode 100644 index 5a51c6ed..00000000 --- a/runtime/server/diarisation_gpu/model_repo/run/config.pbtxt +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: "run" -backend: "python" -max_batch_size: 128 - -input [ - { - name: "input" - data_type: TYPE_FP32 - dims: [ -1 ] - } -] - -output [ - { - name: "LABELS" - data_type: TYPE_FP32 - dims: [ -1, 3 ] - } -] - -dynamic_batching { - preferred_batch_size: [ 16, 32 ] - } -instance_group [ - { - count: 2 - kind: KIND_GPU - } -]