diff --git a/.github/workflows/gigaam.yml b/.github/workflows/gigaam.yml index 36b166f..966c997 100644 --- a/.github/workflows/gigaam.yml +++ b/.github/workflows/gigaam.yml @@ -55,7 +55,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip wheel setuptools pip install --no-cache-dir torch==2.8.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cpu pip install --no-cache-dir -e ".[longform,tests]" @@ -134,15 +134,15 @@ jobs: - name: Check code formatting with black run: | - black --check --diff gigaam/ tests/ + black --check --diff gigaam/ tests/ triton_scripts/ - name: Check imports with isort run: | - isort --check-only --diff gigaam/ tests/ + isort --check-only --diff gigaam/ tests/ triton_scripts/ - name: Lint with flake8 run: | - flake8 --ignore=E203,W503,W504 --max-line-length=120 --statistics gigaam/ tests/ + flake8 --ignore=E203,W503,W504 --max-line-length=120 --statistics gigaam/ tests/ triton_scripts/ - name: Type check with mypy run: | diff --git a/README.md b/README.md index 358d287..2cdf132 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,10 @@ model = AutoModel.from_pretrained("ai-sage/GigaAM-v3", revision="e2e_rnnt", trus These and more advanced (e.g. custom audio loading, batching) examples can be found in the [Colab notebook](https://colab.research.google.com/github/salute-developers/GigaAM/blob/main/colab_example.ipynb). +### Triton Inference Server and TensorRT + +All speech recognition models can also be used in a server environment in ONNX/TRT format through Triton Inference Server. For setup instructions, model conversion, and deployment details, see the [Triton Inference Server documentation](./triton_scripts/README.md). + --- ## Citation diff --git a/README_ru.md b/README_ru.md index a653fa6..846651e 100644 --- a/README_ru.md +++ b/README_ru.md @@ -159,6 +159,10 @@ model = AutoModel.from_pretrained("ai-sage/GigaAM-v3", revision="e2e_rnnt", trus Эти и более продвинутые примеры (кастомная загрузка аудио, батчинг) доступны в [Colab notebook](https://colab.research.google.com/github/salute-developers/GigaAM/blob/main/colab_example.ipynb). +### Triton Inference Server и TensorRT + +Все модели распознавания речи также можно использовать в серверном окружении в формате ONNX/TRT через Triton Inference Server. Инструкции по настройке, конвертации моделей и развёртыванию описаны в [документации Triton Inference Server](./triton_scripts/README.md). + --- ## Citation diff --git a/gigaam/decoding.py b/gigaam/decoding.py index 87c1b5d..aae4b9f 100644 --- a/gigaam/decoding.py +++ b/gigaam/decoding.py @@ -59,23 +59,23 @@ def decode( head: "CTCHead", encoded: Tensor, lengths: Tensor, + labels: Optional[Tensor] = None, ) -> List[Tuple[List[int], List[int]]]: """ CTC greedy decode: returns (token_ids, token_frames) per sample. Token frames are time indices (0..T-1) where a token is emitted. + If labels are provided, encoded and head are not used. """ - log_probs = head(encoder_output=encoded) - assert ( - log_probs.ndim == 3 - ), f"Expected log_probs [B,T,C], got {tuple(log_probs.shape)}" - B, T, C = log_probs.shape - assert ( - C == len(self.tokenizer) + 1 - ), f"Num classes {C} != len(vocab)+1 {len(self.tokenizer)+1}" - - labels = log_probs.argmax(dim=-1) + if labels is None: + log_probs = head(encoder_output=encoded) + C = log_probs.shape[-1] + assert ( + C == len(self.tokenizer) + 1 + ), f"Num classes {C} != len(vocab)+1 {len(self.tokenizer)+1}" + labels = log_probs.argmax(dim=-1) + + B, T = labels.shape device = labels.device - lengths = lengths.to(device=device).clamp(min=0, max=T) skip_mask = labels != self.blank_id diff --git a/gigaam/encoder.py b/gigaam/encoder.py index d49e630..55f7b45 100644 --- a/gigaam/encoder.py +++ b/gigaam/encoder.py @@ -1,5 +1,6 @@ import math from abc import ABC, abstractmethod +from contextlib import contextmanager from typing import Dict, List, Optional, Tuple, Union import torch @@ -514,12 +515,13 @@ def __init__( def input_example( self, - batch_size: int = 1, + batch_size: int = 8, seqlen: int = 200, ) -> Tuple[Tensor, Tensor]: device = next(self.parameters()).device - features = torch.zeros(batch_size, self.feat_in, seqlen) - feature_lengths = torch.full([batch_size], features.shape[-1]) + features = torch.randn(batch_size, self.feat_in, seqlen) + feature_lengths = torch.randint(1, seqlen + 1, (batch_size,)) + feature_lengths[0] = seqlen return features.float().to(device), feature_lengths.to(device) def input_names(self) -> List[str]: @@ -528,6 +530,21 @@ def input_names(self) -> List[str]: def output_names(self) -> List[str]: return ["encoded", "encoded_len"] + @contextmanager + def onnx_export_mode(self): + saved = [] + for layer in self.layers: + attn = layer.self_attn + saved.append((attn.flash_attn, attn.torch_sdpa_attn)) + attn.flash_attn = False + attn.torch_sdpa_attn = False + try: + yield + finally: + for layer, (fa, sdpa) in zip(self.layers, saved): + layer.self_attn.flash_attn = fa + layer.self_attn.torch_sdpa_attn = sdpa + def dynamic_axes(self) -> Dict[str, Dict[int, str]]: return { "audio_signal": {0: "batch_size", 2: "seq_len"}, diff --git a/gigaam/model.py b/gigaam/model.py index aefe189..0ac91d3 100644 --- a/gigaam/model.py +++ b/gigaam/model.py @@ -65,7 +65,8 @@ def to_onnx(self, dir_path: str = ".") -> None: """ Export onnx model encoder to the specified dir. """ - self._to_onnx(dir_path) + with self.encoder.onnx_export_mode(): + self._to_onnx(dir_path) omegaconf.OmegaConf.save(self.cfg, f"{dir_path}/{self.cfg.model_name}.yaml") def _to_onnx(self, dir_path: str = ".") -> None: diff --git a/gigaam/onnx_utils.py b/gigaam/onnx_utils.py index 887d5c3..1ae8c59 100644 --- a/gigaam/onnx_utils.py +++ b/gigaam/onnx_utils.py @@ -18,38 +18,46 @@ def infer_onnx( - wav_file: str, + wav_file: Optional[str], model_cfg: omegaconf.DictConfig, - sessions: List[rt.InferenceSession], + sessions: List[Optional[rt.InferenceSession]], + enc_features: Optional[np.ndarray] = None, preprocessor: Optional[FeatureExtractor] = None, tokenizer: Optional[Tokenizer] = None, ) -> Union[str, np.ndarray]: - """Run ONNX sessions for the model, requires preprocessor instantiating""" + """ + Run ONNX sessions for the model, requires preprocessor instantiating. + The first session (the encoder one) and wav_file can be None if enc_features is provided. + """ model_name = model_cfg.model_name - if preprocessor is None: + assert ( + enc_features is not None or sessions[0] is not None + ), "At least one of encoder session or enc_features is required" + + if preprocessor is None and enc_features is None: preprocessor = hydra.utils.instantiate(model_cfg.preprocessor) if tokenizer is None and ("ctc" in model_name or "rnnt" in model_name): tokenizer = hydra.utils.instantiate(model_cfg.decoding).tokenizer - sgn = load_audio(wav_file) - input_signal = ( - preprocessor(sgn.unsqueeze(0), torch.tensor([sgn.shape[-1]]))[0] - .detach() - .numpy() - ) - - enc_sess = sessions[0] - enc_inputs = { - node.name: data - for (node, data) in zip( - enc_sess.get_inputs(), - [input_signal.astype(DTYPE), [input_signal.shape[-1]]], + if enc_features is None: + sgn = load_audio(wav_file) + input_signal = ( + preprocessor(sgn.unsqueeze(0), torch.tensor([sgn.shape[-1]]))[0] + .detach() + .numpy() ) - } - enc_features = enc_sess.run( - [node.name for node in enc_sess.get_outputs()], enc_inputs - )[0] + enc_sess = sessions[0] + enc_inputs = { + node.name: data + for (node, data) in zip( + enc_sess.get_inputs(), + [input_signal.astype(DTYPE), [input_signal.shape[-1]]], + ) + } + enc_features = enc_sess.run( + [node.name for node in enc_sess.get_outputs()], enc_inputs + )[0] if "emo" in model_name or "ssl" in model_name: return enc_features diff --git a/triton_scripts/Dockerfile b/triton_scripts/Dockerfile new file mode 100644 index 0000000..0f6d143 --- /dev/null +++ b/triton_scripts/Dockerfile @@ -0,0 +1,10 @@ +FROM nvcr.io/nvidia/tritonserver:24.10-py3 + +RUN pip install --no-cache-dir \ + "torch>=2.6,<2.11" \ + "torchaudio>=2.6,<2.11" \ + sentencepiece \ + omegaconf \ + onnxruntime-gpu \ + tqdm \ + hydra-core diff --git a/triton_scripts/README.md b/triton_scripts/README.md new file mode 100644 index 0000000..b643d65 --- /dev/null +++ b/triton_scripts/README.md @@ -0,0 +1,82 @@ +# Triton Inference Server Setup + +This setup supports all ASR models from the GigaAM family. Inference is implemented through a Triton ensemble: the client sends WAV files and receives transcribed texts. CTC models are converted to ONNX/TRT entirely, while RNNT models are split into encoder (ONNX/TRT) and decoder/joint components that run in Python using onnxruntime. + +## Prerequisites + +Navigate to the triton_scripts directory: +```bash +cd triton_scripts +``` + +## 0. Build Docker Image + +Build the Triton Inference Server Docker image: +```bash +docker build -t gigaam-triton . +``` + +## 1. Convert Models to ONNX + +Convert models to ONNX format. This creates `.onnx` checkpoints and configs: +```bash +python run_convert_onnx.py # e.g., v3_ctc, v3_e2e_rnnt +``` + +**Note:** The script saves model configs to the preprocessing directory. For `v3` family models, preprocessing differs from earlier versions. Since Triton uses a shared preprocessing model, you can only use models with the same preprocessing simultaneously (either all `v3` models or all earlier models). The preprocessing is determined by the last model converted to ONNX. + +## 2. Convert ONNX to TensorRT + +Convert ONNX models to TensorRT format. This converts the version of the corresponding CTC/RNNT model that was last converted to ONNX. Run inside the TensorRT Docker container: +```bash +docker run --gpus all -it --rm -v $(pwd):/workspace nvcr.io/nvidia/tensorrt:24.10-py3 +# inside the container: +bash run_convert_trt.sh +``` + +## 3. Start Triton Server + +Run the Triton Inference Server: +```bash +docker run --gpus all --ipc=host -p 8000:8000 -p 8001:8001 -p 8002:8002 \ + -v "$(pwd)/repos:/models" \ + -v "$(pwd)/..:/opt/gigaam_repo" \ + -e PYTHONPATH=/opt/gigaam_repo \ + gigaam-triton \ + tritonserver --model-repository=/models --exit-on-error=false +``` + +Python backend models (e.g. [`rnnt_postprocessing`](repos/rnnt_postprocessing/1/model.py)) do `import gigaam`. The package lives next to this directory, under `gigaam_repo/gigaam/`. + +**Note:** For ONNX models, the default configuration uses `instance_group [{ kind: KIND_GPU }]`. To enable CPU execution, update the `instance_group` to `KIND_CPU` in the following model config files [`ctc`](repos/ctc_encoder_onnx/config.pbtxt), [`rnnt`](repos/gigaam_encoder_onnx/config.pbtxt). + +## 4. Run Client + +Run inference using the client: +```bash +python run_client.py [wav_file2] ... +``` + +Arguments: +- `model_type`: `ctc` or `rnnt` +- `backend`: `onnx` or `trt` +- `wav_file1`, `wav_file2`, ...: Paths to WAV files + +Examples: +```bash +python run_client.py rnnt onnx example.wav +python run_client.py ctc trt audio1.wav audio2.wav audio3.wav +``` + +## Benchmark + +Forward pass time in seconds on CUDA for the first 4 segments from `long_example.wav` (VAD-segmented, ~65s total audio). For torch/onnx — both single-sample and batched inference are shown. + +| Backend | v3_ctc | v3_e2e_rnnt | +|:--------------|:----------------|:----------------| +| triton/trt | 0.034 ± 0.000 | 0.403 ± 0.008 | +| triton/onnx | 0.046 ± 0.001 | 0.413 ± 0.005 | +| onnx (batch) | 0.037 ± 0.004 | 0.949 ± 0.017 | +| onnx | 0.047 ± 0.001 | 1.093 ± 0.045 | +| torch (batch) | 0.036 ± 0.002 | 0.919 ± 0.002 | +| torch | 0.112 ± 0.003 | 1.008 ± 0.001 | diff --git a/triton_scripts/repos/ctc_encoder_onnx/config.pbtxt b/triton_scripts/repos/ctc_encoder_onnx/config.pbtxt new file mode 100644 index 0000000..864d274 --- /dev/null +++ b/triton_scripts/repos/ctc_encoder_onnx/config.pbtxt @@ -0,0 +1,33 @@ +name: "ctc_encoder_onnx" +platform: "onnxruntime_onnx" +max_batch_size: 0 + +input [ + { + name: "features" + data_type: TYPE_FP32 + dims: [-1, 64, -1] + }, + { + name: "feature_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "token_ids" + data_type: TYPE_INT64 + dims: [-1, -1] + }, + { + name: "token_ids_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +instance_group [{ kind: KIND_GPU }] + +parameters { key: "cudnn_conv_algo_search" value: { string_value: "1"} } diff --git a/triton_scripts/repos/ctc_encoder_trt/config.pbtxt b/triton_scripts/repos/ctc_encoder_trt/config.pbtxt new file mode 100644 index 0000000..49f2a64 --- /dev/null +++ b/triton_scripts/repos/ctc_encoder_trt/config.pbtxt @@ -0,0 +1,31 @@ +name: "ctc_encoder_trt" +platform: "tensorrt_plan" +max_batch_size: 0 + +input [ + { + name: "features" + data_type: TYPE_FP32 + dims: [-1, 64, -1] + }, + { + name: "feature_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "token_ids" + data_type: TYPE_INT64 + dims: [-1, -1] + }, + { + name: "token_ids_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +instance_group [{ kind: KIND_GPU }] diff --git a/triton_scripts/repos/ctc_postprocessing/1/model.py b/triton_scripts/repos/ctc_postprocessing/1/model.py new file mode 100644 index 0000000..a0a3269 --- /dev/null +++ b/triton_scripts/repos/ctc_postprocessing/1/model.py @@ -0,0 +1,66 @@ +import os +from typing import Any, Dict, List + +import numpy as np +import omegaconf +import torch + +from gigaam.decoding import CTCGreedyDecoding + + +class TritonPythonModel: + def initialize(self, args: Dict[str, Any]) -> None: + model_version = args["model_version"] + model_repository = args["model_repository"] + + config_path = os.path.join(model_repository, model_version, "config.yaml") + + if os.path.exists(config_path): + cfg = omegaconf.OmegaConf.load(config_path) + else: + raise FileNotFoundError(f"Config file not found: {config_path}") + + vocab = cfg.decoding.get("vocabulary") + if cfg.decoding.get("model_path"): + tokenizer_path = os.path.join( + model_repository, + model_version, + f"{cfg.model_name}_tokenizer.model", + ) + else: + tokenizer_path = None + + self.decoding = CTCGreedyDecoding(vocabulary=vocab, model_path=tokenizer_path) + + def execute(self, requests: Any) -> List[Any]: + import triton_python_backend_utils as pb_utils # type: ignore + + responses: List[Any] = [] + + for request in requests: + token_ids = pb_utils.get_input_tensor_by_name(request, "token_ids") + token_ids_lengths = pb_utils.get_input_tensor_by_name( + request, "token_ids_lengths" + ) + + token_ids_np = token_ids.as_numpy() + token_ids_lengths_np = token_ids_lengths.as_numpy() + + results = self.decoding.decode( + head=None, + encoded=None, + lengths=torch.from_numpy(token_ids_lengths_np), + labels=torch.from_numpy(token_ids_np), + ) + texts = [self.decoding.tokenizer.decode(result[0]) for result in results] + + texts_bytes = [text.encode("utf-8") for text in texts] + texts_array = np.array(texts_bytes, dtype=object) + + output_tensors = [ + pb_utils.Tensor("texts", texts_array), + ] + response = pb_utils.InferenceResponse(output_tensors=output_tensors) + responses.append(response) + + return responses diff --git a/triton_scripts/repos/ctc_postprocessing/config.pbtxt b/triton_scripts/repos/ctc_postprocessing/config.pbtxt new file mode 100644 index 0000000..797a8f4 --- /dev/null +++ b/triton_scripts/repos/ctc_postprocessing/config.pbtxt @@ -0,0 +1,27 @@ +name: "ctc_postprocessing" +backend: "python" +max_batch_size: 0 + +input [ + { + name: "token_ids" + data_type: TYPE_INT64 + dims: [-1, -1] + }, + { + name: "token_ids_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +instance_group [{ kind: KIND_CPU }] + diff --git a/triton_scripts/repos/gigaam_ctc_onnx/1/.gitkeep b/triton_scripts/repos/gigaam_ctc_onnx/1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/triton_scripts/repos/gigaam_ctc_onnx/config.pbtxt b/triton_scripts/repos/gigaam_ctc_onnx/config.pbtxt new file mode 100644 index 0000000..6c1f008 --- /dev/null +++ b/triton_scripts/repos/gigaam_ctc_onnx/config.pbtxt @@ -0,0 +1,85 @@ +name: "gigaam_ctc_onnx" +platform: "ensemble" +max_batch_size: 0 + +input [ + { + name: "audio_batch" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "audio_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +ensemble_scheduling { + step [ + { + model_name: "preprocessing" + model_version: -1 + input_map { + key: "audio_batch" + value: "audio_batch" + } + input_map { + key: "audio_lengths" + value: "audio_lengths" + } + output_map { + key: "features" + value: "preprocessed_features" + } + output_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + }, + { + model_name: "ctc_encoder_onnx" + model_version: -1 + input_map { + key: "features" + value: "preprocessed_features" + } + input_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + output_map { + key: "token_ids" + value: "encoder_token_ids" + } + output_map { + key: "token_ids_lengths" + value: "encoder_token_ids_lengths" + } + }, + { + model_name: "ctc_postprocessing" + model_version: -1 + input_map { + key: "token_ids" + value: "encoder_token_ids" + } + input_map { + key: "token_ids_lengths" + value: "encoder_token_ids_lengths" + } + output_map { + key: "texts" + value: "texts" + } + } + ] +} diff --git a/triton_scripts/repos/gigaam_ctc_trt/1/.gitkeep b/triton_scripts/repos/gigaam_ctc_trt/1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/triton_scripts/repos/gigaam_ctc_trt/config.pbtxt b/triton_scripts/repos/gigaam_ctc_trt/config.pbtxt new file mode 100644 index 0000000..7566c3f --- /dev/null +++ b/triton_scripts/repos/gigaam_ctc_trt/config.pbtxt @@ -0,0 +1,85 @@ +name: "gigaam_ctc_trt" +platform: "ensemble" +max_batch_size: 0 + +input [ + { + name: "audio_batch" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "audio_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +ensemble_scheduling { + step [ + { + model_name: "preprocessing" + model_version: -1 + input_map { + key: "audio_batch" + value: "audio_batch" + } + input_map { + key: "audio_lengths" + value: "audio_lengths" + } + output_map { + key: "features" + value: "preprocessed_features" + } + output_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + }, + { + model_name: "ctc_encoder_trt" + model_version: -1 + input_map { + key: "features" + value: "preprocessed_features" + } + input_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + output_map { + key: "token_ids" + value: "encoder_token_ids" + } + output_map { + key: "token_ids_lengths" + value: "encoder_token_ids_lengths" + } + }, + { + model_name: "ctc_postprocessing" + model_version: -1 + input_map { + key: "token_ids" + value: "encoder_token_ids" + } + input_map { + key: "token_ids_lengths" + value: "encoder_token_ids_lengths" + } + output_map { + key: "texts" + value: "texts" + } + } + ] +} diff --git a/triton_scripts/repos/gigaam_encoder_onnx/config.pbtxt b/triton_scripts/repos/gigaam_encoder_onnx/config.pbtxt new file mode 100644 index 0000000..47645f4 --- /dev/null +++ b/triton_scripts/repos/gigaam_encoder_onnx/config.pbtxt @@ -0,0 +1,33 @@ +name: "gigaam_encoder_onnx" +platform: "onnxruntime_onnx" +max_batch_size: 0 + +input [ + { + name: "audio_signal" + data_type: TYPE_FP32 + dims: [-1, 64, -1] + }, + { + name: "length" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "encoded" + data_type: TYPE_FP32 + dims: [-1, 768, -1] + }, + { + name: "encoded_len" + data_type: TYPE_INT32 + dims: [-1] + } +] + +instance_group [{ kind: KIND_GPU }] + +parameters { key: "cudnn_conv_algo_search" value: { string_value: "1"} } diff --git a/triton_scripts/repos/gigaam_encoder_trt/config.pbtxt b/triton_scripts/repos/gigaam_encoder_trt/config.pbtxt new file mode 100644 index 0000000..2a746cb --- /dev/null +++ b/triton_scripts/repos/gigaam_encoder_trt/config.pbtxt @@ -0,0 +1,31 @@ +name: "gigaam_encoder_trt" +platform: "tensorrt_plan" +max_batch_size: 0 + +input [ + { + name: "audio_signal" + data_type: TYPE_FP32 + dims: [-1, 64, -1] + }, + { + name: "length" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "encoded" + data_type: TYPE_FP32 + dims: [-1, 768, -1] + }, + { + name: "encoded_len" + data_type: TYPE_INT32 + dims: [-1] + } +] + +instance_group [{ kind: KIND_GPU }] diff --git a/triton_scripts/repos/gigaam_rnnt_onnx/1/.gitkeep b/triton_scripts/repos/gigaam_rnnt_onnx/1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/triton_scripts/repos/gigaam_rnnt_onnx/config.pbtxt b/triton_scripts/repos/gigaam_rnnt_onnx/config.pbtxt new file mode 100644 index 0000000..ae5264c --- /dev/null +++ b/triton_scripts/repos/gigaam_rnnt_onnx/config.pbtxt @@ -0,0 +1,85 @@ +name: "gigaam_rnnt_onnx" +platform: "ensemble" +max_batch_size: 0 + +input [ + { + name: "audio_batch" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "audio_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +ensemble_scheduling { + step [ + { + model_name: "preprocessing" + model_version: -1 + input_map { + key: "audio_batch" + value: "audio_batch" + } + input_map { + key: "audio_lengths" + value: "audio_lengths" + } + output_map { + key: "features" + value: "preprocessed_features" + } + output_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + }, + { + model_name: "gigaam_encoder_onnx" + model_version: -1 + input_map { + key: "audio_signal" + value: "preprocessed_features" + } + input_map { + key: "length" + value: "preprocessed_lengths" + } + output_map { + key: "encoded" + value: "encoder_encoded" + } + output_map { + key: "encoded_len" + value: "encoder_encoded_len" + } + }, + { + model_name: "rnnt_postprocessing" + model_version: -1 + input_map { + key: "encoded" + value: "encoder_encoded" + } + input_map { + key: "encoded_lengths" + value: "encoder_encoded_len" + } + output_map { + key: "texts" + value: "texts" + } + } + ] +} diff --git a/triton_scripts/repos/gigaam_rnnt_trt/1/.gitkeep b/triton_scripts/repos/gigaam_rnnt_trt/1/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/triton_scripts/repos/gigaam_rnnt_trt/config.pbtxt b/triton_scripts/repos/gigaam_rnnt_trt/config.pbtxt new file mode 100644 index 0000000..964707e --- /dev/null +++ b/triton_scripts/repos/gigaam_rnnt_trt/config.pbtxt @@ -0,0 +1,85 @@ +name: "gigaam_rnnt_trt" +platform: "ensemble" +max_batch_size: 0 + +input [ + { + name: "audio_batch" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "audio_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +ensemble_scheduling { + step [ + { + model_name: "preprocessing" + model_version: -1 + input_map { + key: "audio_batch" + value: "audio_batch" + } + input_map { + key: "audio_lengths" + value: "audio_lengths" + } + output_map { + key: "features" + value: "preprocessed_features" + } + output_map { + key: "feature_lengths" + value: "preprocessed_lengths" + } + }, + { + model_name: "gigaam_encoder_trt" + model_version: -1 + input_map { + key: "audio_signal" + value: "preprocessed_features" + } + input_map { + key: "length" + value: "preprocessed_lengths" + } + output_map { + key: "encoded" + value: "encoder_encoded" + } + output_map { + key: "encoded_len" + value: "encoder_encoded_len" + } + }, + { + model_name: "rnnt_postprocessing" + model_version: -1 + input_map { + key: "encoded" + value: "encoder_encoded" + } + input_map { + key: "encoded_lengths" + value: "encoder_encoded_len" + } + output_map { + key: "texts" + value: "texts" + } + } + ] +} diff --git a/triton_scripts/repos/preprocessing/1/model.py b/triton_scripts/repos/preprocessing/1/model.py new file mode 100644 index 0000000..c3ead81 --- /dev/null +++ b/triton_scripts/repos/preprocessing/1/model.py @@ -0,0 +1,88 @@ +import os +import sys +import warnings +from typing import Any, Dict, List + +import numpy as np +import omegaconf +import torch +from torch import Tensor + +from gigaam.preprocess import FeatureExtractor + +warnings.simplefilter("ignore", category=UserWarning) + + +SAMPLE_RATE = 16000 + + +class TritonPythonModel: + def initialize(self, args: Dict[str, Any]) -> None: + model_version = args["model_version"] + model_repository = args["model_repository"] + + config_path = os.path.join(model_repository, model_version, "config.yaml") + + cfg = omegaconf.OmegaConf.load(config_path) + + # Check if model is v3 and warn if not + if not cfg.model_name.startswith("v3"): + sys.stderr.write( + f"Model '{cfg.model_name}' does not belong to 'v3' family. " + "Using old feature extraction version " + "(incompatible with v3 models)." + ) + sys.stderr.flush() + + preprocessor_dict = omegaconf.OmegaConf.to_container( + cfg.preprocessor, resolve=True + ) + preprocessor_dict.pop("_target_", None) + self.preprocessor = FeatureExtractor(**preprocessor_dict) + self.preprocessor.eval() + + def execute(self, requests: Any) -> List[Any]: + import triton_python_backend_utils as pb_utils # type: ignore + + responses: List[Any] = [] + + for request in requests: + audio_batch = pb_utils.get_input_tensor_by_name(request, "audio_batch") + audio_lengths = pb_utils.get_input_tensor_by_name(request, "audio_lengths") + + audio_batch_np = audio_batch.as_numpy() + audio_lengths_np = audio_lengths.as_numpy() + + audio_tensors: List[Tensor] = [] + start_idx = 0 + for length in audio_lengths_np: + length_int = int(length) + audio_tensors.append( + torch.from_numpy( + audio_batch_np[start_idx : start_idx + length_int] + ).float() + ) + start_idx += length_int + + batch_audio = torch.nn.utils.rnn.pad_sequence( + audio_tensors, batch_first=True + ) + batch_lengths = torch.tensor(audio_lengths_np, dtype=torch.long) + + with torch.no_grad(): + features, feature_lengths = self.preprocessor( + batch_audio, batch_lengths + ) + + features_np = features.detach().cpu().numpy().astype(np.float32) + feature_lengths_np = feature_lengths.detach().cpu().numpy().astype(np.int64) + + output_tensors = [ + pb_utils.Tensor("features", features_np), + pb_utils.Tensor("feature_lengths", feature_lengths_np), + ] + + response = pb_utils.InferenceResponse(output_tensors=output_tensors) + responses.append(response) + + return responses diff --git a/triton_scripts/repos/preprocessing/config.pbtxt b/triton_scripts/repos/preprocessing/config.pbtxt new file mode 100644 index 0000000..01a5de2 --- /dev/null +++ b/triton_scripts/repos/preprocessing/config.pbtxt @@ -0,0 +1,32 @@ +name: "preprocessing" +backend: "python" +max_batch_size: 0 + +input [ + { + name: "audio_batch" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "audio_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +output [ + { + name: "features" + data_type: TYPE_FP32 + dims: [-1, 64, -1] + }, + { + name: "feature_lengths" + data_type: TYPE_INT64 + dims: [-1] + } +] + +instance_group [{ kind: KIND_CPU }] + diff --git a/triton_scripts/repos/rnnt_postprocessing/1/model.py b/triton_scripts/repos/rnnt_postprocessing/1/model.py new file mode 100644 index 0000000..cabaca4 --- /dev/null +++ b/triton_scripts/repos/rnnt_postprocessing/1/model.py @@ -0,0 +1,109 @@ +import os +from typing import Any, Dict, List + +import numpy as np +import omegaconf +import onnxruntime as rt + +from gigaam.decoding import Tokenizer +from gigaam.onnx_utils import infer_onnx + + +class TritonPythonModel: + def initialize(self, args: Dict[str, Any]) -> None: + model_version = args["model_version"] + model_repository = args["model_repository"] + + config_path = os.path.join(model_repository, model_version, "config.yaml") + + if os.path.exists(config_path): + cfg = omegaconf.OmegaConf.load(config_path) + else: + raise FileNotFoundError(f"Config file not found: {config_path}") + + model_name = cfg.model_name + vocab = cfg.decoding.get("vocabulary") + + if cfg.decoding.get("model_path"): + tokenizer_path = os.path.join( + model_repository, + model_version, + f"{model_name}_tokenizer.model", + ) + else: + tokenizer_path = None + + self.cfg = cfg + self.tokenizer = Tokenizer(vocab=vocab, model_path=tokenizer_path) + + # Load ONNX models + decoder_path = os.path.join( + model_repository, model_version, f"{model_name}_decoder.onnx" + ) + joint_path = os.path.join( + model_repository, model_version, f"{model_name}_joint.onnx" + ) + + if not os.path.exists(decoder_path): + raise FileNotFoundError(f"Decoder ONNX model not found: {decoder_path}") + if not os.path.exists(joint_path): + raise FileNotFoundError(f"Joint ONNX model not found: {joint_path}") + + available_providers = rt.get_available_providers() + provider = ( + "CUDAExecutionProvider" + if "CUDAExecutionProvider" in available_providers + else "CPUExecutionProvider" + ) + opts = rt.SessionOptions() + opts.intra_op_num_threads = 16 + opts.log_severity_level = 3 + + self.pred_sess = rt.InferenceSession( + decoder_path, + providers=[provider], + sess_options=opts, + ) + self.joint_sess = rt.InferenceSession( + joint_path, + providers=[provider], + sess_options=opts, + ) + + self.pred_hidden = cfg.head.decoder.pred_hidden + + def execute(self, requests: Any) -> List[Any]: + import triton_python_backend_utils as pb_utils # type: ignore + + responses: List[Any] = [] + + for request in requests: + encoded = pb_utils.get_input_tensor_by_name(request, "encoded") + encoded_lengths = pb_utils.get_input_tensor_by_name( + request, "encoded_lengths" + ) + + encoded_np = encoded.as_numpy() + encoded_lengths_np = encoded_lengths.as_numpy() + + texts = [ + infer_onnx( + wav_file=None, + model_cfg=self.cfg, + sessions=[None, self.pred_sess, self.joint_sess], + enc_features=encoded_np[i : i + 1, :, : encoded_lengths_np[i]], + tokenizer=self.tokenizer, + ) + for i in range(encoded_np.shape[0]) + ] + + texts_bytes = [text.encode("utf-8") for text in texts] + texts_array = np.array(texts_bytes, dtype=object) + + output_tensors = [ + pb_utils.Tensor("texts", texts_array), + ] + response = pb_utils.InferenceResponse(output_tensors=output_tensors) + responses.append(response) + + return responses diff --git a/triton_scripts/repos/rnnt_postprocessing/config.pbtxt b/triton_scripts/repos/rnnt_postprocessing/config.pbtxt new file mode 100644 index 0000000..01efa2f --- /dev/null +++ b/triton_scripts/repos/rnnt_postprocessing/config.pbtxt @@ -0,0 +1,26 @@ +name: "rnnt_postprocessing" +backend: "python" +max_batch_size: 0 + +input [ + { + name: "encoded" + data_type: TYPE_FP32 + dims: [-1, 768, -1] + }, + { + name: "encoded_lengths" + data_type: TYPE_INT32 + dims: [-1] + } +] + +output [ + { + name: "texts" + data_type: TYPE_STRING + dims: [-1] + } +] + +instance_group [{ kind: KIND_CPU }] diff --git a/triton_scripts/run_client.py b/triton_scripts/run_client.py new file mode 100644 index 0000000..9040bdd --- /dev/null +++ b/triton_scripts/run_client.py @@ -0,0 +1,98 @@ +import sys +from typing import List + +import librosa +import numpy as np +from tritonclient.http import InferenceServerClient, InferInput + +SAMPLE_RATE = 16000 + + +def infer_ensemble( + wav_paths: List[str], + model_type: str = "ctc", + backend: str = "onnx", + triton_url: str = "localhost:8000", + timeout: int = 60, +) -> List[str]: + """ + Run inference on ensemble model. + + Args: + wav_paths: List of paths to WAV files + model_type: Type of model - "ctc" or "rnnt" + backend: Backend type - "onnx" or "trt" + triton_url: Triton server URL (with or without http:// prefix) + timeout: Request timeout in seconds + + Returns: + List of transcribed texts + """ + if model_type not in ["ctc", "rnnt"]: + raise ValueError(f"Invalid model_type: {model_type}. Must be 'ctc' or 'rnnt'") + if backend not in ["onnx", "trt"]: + raise ValueError(f"Invalid backend: {backend}. Must be 'onnx' or 'trt'") + + model_name = f"gigaam_{model_type}_{backend}" + + if triton_url.startswith("http://"): + triton_url = triton_url[7:] + elif triton_url.startswith("https://"): + triton_url = triton_url[8:] + + if triton_url.startswith("localhost"): + triton_url = triton_url.replace("localhost", "127.0.0.1") + + audio_arrays: List[np.ndarray] = [] + audio_lengths: List[int] = [] + + for wav_path in wav_paths: + audio = librosa.load(wav_path, sr=SAMPLE_RATE, mono=True)[0] + audio_arrays.append(audio.astype(np.float32)) + audio_lengths.append(len(audio)) + + audio_batch = np.concatenate(audio_arrays).astype(np.float32) + audio_lengths_array = np.array(audio_lengths, dtype=np.int64) + + client = InferenceServerClient( + url=triton_url, + connection_timeout=timeout, + network_timeout=timeout, + ) + + input_audio = InferInput("audio_batch", audio_batch.shape, "FP32") + input_audio.set_data_from_numpy(audio_batch) + + input_lengths = InferInput("audio_lengths", audio_lengths_array.shape, "INT64") + input_lengths.set_data_from_numpy(audio_lengths_array) + + response = client.infer(model_name, [input_audio, input_lengths]) + + texts_bytes = response.as_numpy("texts") + texts = [text_bytes.decode("utf-8") for text_bytes in texts_bytes] + + return texts + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print( + "Usage: python run_client.py " + " [wav_file2] ..." + ) + print(" model_type: ctc | rnnt") + print(" backend: onnx | trt") + sys.exit(1) + + model_type = sys.argv[1] + backend = sys.argv[2] + wav_files = sys.argv[3:] + + if not wav_files: + print("Error: No WAV files provided") + sys.exit(1) + + texts = infer_ensemble(wav_files, model_type=model_type, backend=backend) + + for wav_file, text in zip(wav_files, texts): + print(f"{wav_file}: {text}") diff --git a/triton_scripts/run_convert_onnx.py b/triton_scripts/run_convert_onnx.py new file mode 100644 index 0000000..620b0a9 --- /dev/null +++ b/triton_scripts/run_convert_onnx.py @@ -0,0 +1,146 @@ +import os +import shutil +import sys +import types +import warnings +from typing import Any, Tuple + +import omegaconf +from torch import Tensor + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import gigaam # noqa: E402 +from gigaam.utils import onnx_converter # noqa: E402 + + +# We need to override the forward method to return the argmax of the logits +# to avoid variable output shapes in the ONNX / TRT and keep the unified config. +def forward_for_export_with_argmax( + self: Any, features: Tensor, feature_lengths: Tensor +) -> Tuple[Tensor, Tensor]: + encoded, encoded_len = self.encoder(features, feature_lengths) + logits = self.head(encoded) + token_ids = logits.argmax(dim=-1) + return token_ids, encoded_len.long() + + +def _to_onnx_with_token_ids(self: Any, dir_path: str = ".") -> None: + """Convert to ONNX with token ids instead of logits.""" + saved_forward = self.forward + self.forward = self.forward_for_export + try: + onnx_converter( + model_name="model", + out_dir=dir_path, + module=self, + inputs=self.encoder.input_example(), + input_names=["features", "feature_lengths"], + output_names=["token_ids", "token_ids_lengths"], + dynamic_axes={ + "features": {0: "batch_size", 2: "seq_len"}, + "feature_lengths": {0: "batch_size"}, + "token_ids": {0: "batch_size", 1: "seq_len"}, + "token_ids_lengths": {0: "batch_size"}, + }, + ) + finally: + self.forward = saved_forward + + +def convert_ctc(model: Any) -> tuple[str, str]: + save_path = "repos/ctc_encoder_onnx/1" + postprocessing_dir = "repos/ctc_postprocessing/1" + + original_forward = model.forward_for_export + original_to_onnx = model._to_onnx + + model.forward_for_export = types.MethodType(forward_for_export_with_argmax, model) + model._to_onnx = types.MethodType(_to_onnx_with_token_ids, model) + + try: + model.to_onnx(save_path) + finally: + model.forward_for_export = original_forward + model._to_onnx = original_to_onnx + + return save_path, postprocessing_dir + + +def convert_rnnt(model: Any) -> tuple[str, str]: + save_path = "repos/gigaam_encoder_onnx/1" + postprocessing_dir = "repos/rnnt_postprocessing/1" + + # Save encoder, decoder and joint parts to onnx + model.to_onnx(save_path) + + rename_onnx( + f"{save_path}/{model.cfg.model_name}_encoder.onnx", + f"{save_path}/model.onnx", + ) + + os.makedirs(postprocessing_dir, exist_ok=True) + for part in ("decoder", "joint"): + src = f"{save_path}/{model.cfg.model_name}_{part}.onnx" + dst = f"{postprocessing_dir}/{model.cfg.model_name}_{part}.onnx" + rename_onnx(src, dst) + + return save_path, postprocessing_dir + + +def rename_onnx(src: str, dst: str) -> None: + if os.path.exists(src): + os.rename(src, dst) + print(f"Moved {src} -> {dst}") + + +def save_and_distribute_config( + model: Any, save_path: str, postprocessing_dir: str +) -> None: + config_path = f"{save_path}/config.yaml" + os.makedirs(os.path.dirname(config_path), exist_ok=True) + omegaconf.OmegaConf.save(model.cfg, config_path) + print(f"Config saved to {config_path}") + + if not model.cfg.model_name.startswith("v3"): + warnings.warn( + f"Model '{model.cfg.model_name}' is not from 'v3' family. " + "Triton preprocessing will use old feature extraction version.", + UserWarning, + ) + + preprocessing_dir = "repos/preprocessing/1" + for target_dir in (preprocessing_dir, postprocessing_dir): + os.makedirs(target_dir, exist_ok=True) + shutil.copy(config_path, f"{target_dir}/config.yaml") + print(f"Config copied to {target_dir}/config.yaml") + + +def copy_tokenizer(model: Any, postprocessing_dir: str) -> None: + tokenizer_path = model.cfg.decoding.get("model_path") + if tokenizer_path and os.path.exists(tokenizer_path): + filename = os.path.basename(tokenizer_path) + shutil.copy(tokenizer_path, f"{postprocessing_dir}/{filename}") + print(f"Tokenizer copied to {postprocessing_dir}/{filename}") + + +def main() -> None: + if len(sys.argv) < 2: + print("Usage: python run_convert_onnx.py ") + sys.exit(1) + + model_version = sys.argv[1] + model = gigaam.load_model(model_version) + + if "ctc" in model_version: + save_path, postprocessing_dir = convert_ctc(model) + else: + save_path, postprocessing_dir = convert_rnnt(model) + + # Save config and tokenizer for the postprocessing + save_and_distribute_config(model, save_path, postprocessing_dir) + copy_tokenizer(model, postprocessing_dir) + + +if __name__ == "__main__": + main() diff --git a/triton_scripts/run_convert_trt.sh b/triton_scripts/run_convert_trt.sh new file mode 100755 index 0000000..c983135 --- /dev/null +++ b/triton_scripts/run_convert_trt.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# Script to convert ONNX models to TensorRT (TRT) format +# Usage: bash run_convert_trt.sh [ctc|rnnt] +# Requires: TensorRT, trtexec in PATH + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if trtexec is available +if ! command -v trtexec &> /dev/null; then + echo -e "${RED}Error: trtexec not found in PATH${NC}" + echo "Please install TensorRT and ensure trtexec is in your PATH" + exit 1 +fi + +# Parse model type argument +MODEL_TYPE=${1:-""} + +if [ -z "$MODEL_TYPE" ]; then + echo -e "${RED}Error: Model type not specified${NC}" + echo "Usage: bash run_convert_trt.sh [ctc|rnnt]" + exit 1 +fi + +if [ "$MODEL_TYPE" != "ctc" ] && [ "$MODEL_TYPE" != "rnnt" ]; then + echo -e "${RED}Error: Invalid model type: $MODEL_TYPE${NC}" + echo "Usage: bash run_convert_trt.sh [ctc|rnnt]" + exit 1 +fi + +# Convert CTC encoder +if [ "$MODEL_TYPE" = "ctc" ]; then + echo -e "${GREEN}=== Converting CTC Encoder ===${NC}" + ctc_onnx="repos/ctc_encoder_onnx/1/model.onnx" + ctc_trt_dir="repos/ctc_encoder_trt/1" + ctc_trt_path="$ctc_trt_dir/model.plan" + + if [ -f "$ctc_onnx" ]; then + mkdir -p "$ctc_trt_dir" + trtexec \ + --onnx="$ctc_onnx" \ + --saveEngine="$ctc_trt_path" \ + --fp16 \ + --memPoolSize=workspace:8192 \ + --minShapes=features:1x64x1,feature_lengths:1 \ + --optShapes=features:8x64x1000,feature_lengths:8 \ + --maxShapes=features:32x64x5000,feature_lengths:32 \ + --verbose \ + --noTF32 + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ CTC encoder converted${NC}" + else + echo -e "${RED}✗ Failed to convert CTC encoder${NC}" + exit 1 + fi + else + echo -e "${RED}Error: CTC encoder ONNX not found: $ctc_onnx${NC}" + echo " Run run_convert_onnx.py v3_ctc first to generate ONNX model" + exit 1 + fi +fi + +# Convert RNNT encoder +if [ "$MODEL_TYPE" = "rnnt" ]; then + echo -e "${GREEN}=== Converting RNNT Encoder ===${NC}" + rnnt_onnx="repos/gigaam_encoder_onnx/1/model.onnx" + rnnt_trt_dir="repos/gigaam_encoder_trt/1" + rnnt_trt_path="$rnnt_trt_dir/model.plan" + + if [ -f "$rnnt_onnx" ]; then + mkdir -p "$rnnt_trt_dir" + trtexec \ + --onnx="$rnnt_onnx" \ + --saveEngine="$rnnt_trt_path" \ + --fp16 \ + --memPoolSize=workspace:8192 \ + --minShapes=audio_signal:1x64x1,length:1 \ + --optShapes=audio_signal:8x64x1000,length:8 \ + --maxShapes=audio_signal:32x64x5000,length:32 \ + --verbose \ + --noTF32 + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ RNNT encoder converted${NC}" + else + echo -e "${RED}✗ Failed to convert RNNT encoder${NC}" + exit 1 + fi + else + echo -e "${RED}Error: RNNT encoder ONNX not found: $rnnt_onnx${NC}" + echo " Run run_convert_onnx.py v3_e2e_rnnt first to generate ONNX model" + exit 1 + fi +fi + +echo -e "${GREEN}=== Conversion Complete ===${NC}"