ba4be087d5
CICD NeMo / cicd-main-unit-tests (push) Blocked by required conditions
CICD NeMo / cicd-main-speech (push) Blocked by required conditions
CICD NeMo / cicd-test-container-build (push) Blocked by required conditions
CICD NeMo / cicd-import-tests (push) Blocked by required conditions
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Blocked by required conditions
CICD NeMo / Nemo_CICD_Test (push) Blocked by required conditions
CICD NeMo / Coverage (e2e) (push) Blocked by required conditions
CICD NeMo / Coverage (unit-test) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
CICD NeMo / cicd-wait-in-queue (push) Waiting to run
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
601 lines
24 KiB
Python
601 lines
24 KiB
Python
# Copyright (c) 2025, 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.
|
|
|
|
"""Streaming beam-search (MALSD + MAES) decoding tests.
|
|
|
|
Beam-search analogue of ``test_streaming_decoding.py``. Exercises the streaming
|
|
path of the batched beam-search computers by manually feeding the encoder
|
|
output to ``model.decoding.decoding.decoding_computer`` in chunks and threading
|
|
``prev_batched_state`` (a ``BatchedBeamState``):
|
|
|
|
- :func:`test_malsd_streaming_batched_state` -- covers RNNT and TDT MALSD
|
|
(:class:`ModifiedALSDBatchedRNNTComputer`, :class:`ModifiedALSDBatchedTDTComputer`)
|
|
across the eager path and both captured-graph variants (``full_graph`` and
|
|
``no_while_loops``).
|
|
- :func:`test_malsd_streaming_batched_state_with_word_boosting` -- same MALSD matrix
|
|
but with a ``GPUBoostingTreeModel`` fusion model plugged in (``boosting_tree.
|
|
key_phrases_list``); exercises cross-chunk restoration of per-beam fusion states
|
|
in :meth:`_init_decoding_state`.
|
|
- :func:`test_maes_streaming_batched_state` -- covers RNNT MAES
|
|
(:class:`ModifiedAESBatchedRNNTComputer`); MAES is RNNT-only and pure-PyTorch,
|
|
so there is no ``is_tdt`` / ``cuda_graphs_mode`` axis.
|
|
|
|
Per-chunk results are merged into a single ``BatchedBeamHyps`` via
|
|
``flatten_()`` + ``merge_(..., is_chunk_continuation=True,
|
|
boundary_prev_ptr=...)`` -- the same accumulation pattern used by the
|
|
cache-aware / chunked streaming inference scripts.
|
|
|
|
Streamed transcripts are asserted to be identical to the non-streaming
|
|
reference produced by ``model.transcribe`` with the same beam settings:
|
|
beam search with ``prev_batched_state`` is chunk-invariant because all
|
|
cross-chunk per-beam state (scores, ``last_label``, decoded lengths,
|
|
decoder + fusion states, ``last_timestamp_lasts``, ...) is preserved across
|
|
boundaries.
|
|
"""
|
|
|
|
import copy
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
import torch
|
|
from omegaconf import open_dict
|
|
from tqdm.auto import tqdm
|
|
|
|
from nemo.collections.asr.models import ASRModel
|
|
from nemo.collections.asr.parts.context_biasing.biasing_multi_model import BiasingRequestItemConfig
|
|
from nemo.collections.asr.parts.context_biasing.boosting_graph_batched import BoostingTreeModelConfig
|
|
from nemo.collections.asr.parts.submodules.transducer_decoding.label_looping_base import BatchedBeamState
|
|
from nemo.collections.asr.parts.utils.batched_beam_decoding_utils import BatchedBeamHyps
|
|
from nemo.collections.asr.parts.utils.manifest_utils import read_manifest
|
|
from tests.collections.asr.decoding.utils import load_audio, make_preprocessor_deterministic
|
|
|
|
|
|
def get_devices_for_testing(use_cpu_always: bool = False) -> list[torch.device]:
|
|
devices = [torch.device("cpu")] if use_cpu_always else []
|
|
if torch.cuda.is_available():
|
|
devices.append(torch.device("cuda:0"))
|
|
|
|
if torch.mps.is_available():
|
|
devices.append(torch.device("mps"))
|
|
|
|
if len(devices) == 0:
|
|
# no fast device for testing, add CPU
|
|
devices.append(torch.device("cpu"))
|
|
return devices
|
|
|
|
|
|
DEVICES = get_devices_for_testing(use_cpu_always=True)
|
|
|
|
|
|
def _make_device_param_matrix() -> list:
|
|
"""
|
|
Build the ``(device, cuda_graphs_mode)`` parametrize entries with explicit, readable
|
|
pytest IDs (``cpu-no-graphs``, ``cuda-full-graph``, ``cuda-no-while-loops``, ...) so the
|
|
test matrix shows which device + graph-mode pair is exercised instead of opaque
|
|
``device0`` / ``device1`` ids.
|
|
|
|
``cuda_graphs_mode`` is ``None`` for the eager path or one of the
|
|
:class:`ModifiedALSDBatched{RNNT,TDT}Computer.CudaGraphsMode` string values
|
|
(``"full_graph"`` / ``"no_while_loops"``) for the two captured-graph variants. The test
|
|
uses ``force_cuda_graphs_mode`` to pin the variant explicitly so each captured path is
|
|
actually exercised (otherwise ``maybe_enable_cuda_graphs`` would always pick
|
|
``full_graph`` whenever conditional nodes are supported).
|
|
|
|
Coverage:
|
|
- every device in ``DEVICES`` with ``cuda_graphs_mode=None`` (eager path).
|
|
- every CUDA device additionally with both ``"full_graph"`` and ``"no_while_loops"``.
|
|
"""
|
|
entries: list = []
|
|
for device in DEVICES:
|
|
entries.append(pytest.param(device, None, id=f"{device.type}-no-graphs"))
|
|
for device in DEVICES:
|
|
if device.type == "cuda":
|
|
entries.append(pytest.param(device, "full_graph", id=f"{device.type}-full-graph"))
|
|
entries.append(pytest.param(device, "no_while_loops", id=f"{device.type}-no-while-loops"))
|
|
return entries
|
|
|
|
|
|
DEVICE_PARAM_MATRIX = _make_device_param_matrix()
|
|
|
|
|
|
def _make_maes_device_param_matrix() -> list:
|
|
"""Build readable ``device`` parametrize entries for MAES tests.
|
|
|
|
MAES has no CUDA-graphs path (it's pure-PyTorch), so the matrix is just one entry per
|
|
available device with explicit IDs (``cpu``, ``cuda``, ...) instead of opaque
|
|
``device0`` / ``device1``.
|
|
"""
|
|
return [pytest.param(device, id=device.type) for device in DEVICES]
|
|
|
|
|
|
MAES_DEVICE_PARAM_MATRIX = _make_maes_device_param_matrix()
|
|
|
|
|
|
def get_model_encoder_output(
|
|
test_audio_filenames,
|
|
num_samples: int,
|
|
model: ASRModel,
|
|
device: torch.device = torch.device("cpu"),
|
|
dtype: torch.dtype = torch.float32,
|
|
):
|
|
audio_filepaths = test_audio_filenames[:num_samples]
|
|
|
|
with torch.no_grad():
|
|
make_preprocessor_deterministic(model)
|
|
model.eval()
|
|
|
|
all_inputs, all_lengths = [], []
|
|
for audio_file in tqdm(audio_filepaths, desc="Loading audio files"):
|
|
audio_tensor, _ = load_audio(audio_file)
|
|
all_inputs.append(audio_tensor)
|
|
all_lengths.append(torch.tensor(audio_tensor.shape[0], dtype=torch.int64))
|
|
|
|
input_batch = torch.nn.utils.rnn.pad_sequence(all_inputs, batch_first=True).to(device=device, dtype=dtype)
|
|
length_batch = torch.tensor(all_lengths, dtype=torch.int64).to(device)
|
|
|
|
encoded_outputs, encoded_length = model(input_signal=input_batch, input_signal_length=length_batch)
|
|
|
|
return encoded_outputs, encoded_length
|
|
|
|
|
|
def get_batch_encoder_outputs_from_records(records, model, device):
|
|
"""Helper function to get encoder outputs for a batch of manifest records"""
|
|
filenames = [record["audio_filepath"] for record in records]
|
|
local_batch_size = len(filenames)
|
|
encoder_output, encoder_output_len = get_model_encoder_output(
|
|
test_audio_filenames=filenames, model=model, num_samples=local_batch_size, device=device
|
|
)
|
|
return encoder_output, encoder_output_len
|
|
|
|
|
|
def _configure_malsd_decoding(
|
|
model: ASRModel,
|
|
cuda_graphs_mode: Optional[str],
|
|
beam_size: int,
|
|
max_symbols: int,
|
|
key_phrases_list: Optional[list[str]] = None,
|
|
boosting_tree_alpha: float = 1.0,
|
|
enable_per_stream_biasing: bool = False,
|
|
) -> None:
|
|
"""Switch ``model`` to the ``malsd_batch`` beam-search strategy used by the streaming tests.
|
|
|
|
``cuda_graphs_mode`` is the CudaGraphsMode string (``"full_graph"`` or
|
|
``"no_while_loops"``) when CUDA graphs should be used, or ``None`` for the eager path.
|
|
When non-None we also call ``force_cuda_graphs_mode`` to pin the variant after the
|
|
decoding strategy is swapped in -- ``maybe_enable_cuda_graphs`` would otherwise auto-pick
|
|
``full_graph`` whenever conditional nodes are supported, leaving the ``no_while_loops``
|
|
branch effectively untested.
|
|
|
|
When ``key_phrases_list`` is given, a ``boosting_tree`` fusion model is plugged in
|
|
(``BoostingTreeModelConfig.key_phrases_list``) so the streaming path exercises
|
|
cross-chunk fusion-state restoration in :meth:`_init_decoding_state`.
|
|
"""
|
|
decoding_cfg = copy.deepcopy(model.cfg.decoding)
|
|
decoding_cfg.strategy = "malsd_batch"
|
|
with open_dict(decoding_cfg):
|
|
decoding_cfg.beam.beam_size = beam_size
|
|
decoding_cfg.beam.max_symbols = max_symbols
|
|
decoding_cfg.beam.allow_cuda_graphs = cuda_graphs_mode is not None
|
|
decoding_cfg.beam.return_best_hypothesis = True
|
|
decoding_cfg.beam.score_norm = True
|
|
if key_phrases_list is not None:
|
|
decoding_cfg.beam.boosting_tree = {"key_phrases_list": list(key_phrases_list)}
|
|
decoding_cfg.beam.boosting_tree_alpha = boosting_tree_alpha
|
|
if enable_per_stream_biasing:
|
|
decoding_cfg.beam.enable_per_stream_biasing = True
|
|
model.change_decoding_strategy(decoding_cfg)
|
|
if cuda_graphs_mode is not None:
|
|
model.decoding.decoding.decoding_computer.force_cuda_graphs_mode(cuda_graphs_mode)
|
|
|
|
|
|
def _configure_maes_decoding(
|
|
model: ASRModel,
|
|
beam_size: int,
|
|
maes_num_steps: int,
|
|
maes_expansion_beta: int,
|
|
maes_expansion_gamma: float,
|
|
) -> None:
|
|
"""Switch ``model`` to the ``maes_batch`` beam-search strategy used by the streaming tests.
|
|
|
|
MAES is RNNT-only and currently pure-PyTorch (CUDA graphs are not implemented;
|
|
``allow_cuda_graphs`` is accepted only for API parity with MALSD and is ignored by
|
|
:class:`ModifiedAESBatchedRNNTComputer`).
|
|
"""
|
|
decoding_cfg = copy.deepcopy(model.cfg.decoding)
|
|
decoding_cfg.strategy = "maes_batch"
|
|
with open_dict(decoding_cfg):
|
|
decoding_cfg.beam.beam_size = beam_size
|
|
decoding_cfg.beam.maes_num_steps = maes_num_steps
|
|
decoding_cfg.beam.maes_expansion_beta = maes_expansion_beta
|
|
decoding_cfg.beam.maes_expansion_gamma = maes_expansion_gamma
|
|
decoding_cfg.beam.allow_cuda_graphs = False
|
|
decoding_cfg.beam.return_best_hypothesis = True
|
|
decoding_cfg.beam.score_norm = True
|
|
model.change_decoding_strategy(decoding_cfg)
|
|
|
|
|
|
def _reset_decoding_computer_state(model: ASRModel) -> None:
|
|
decoding_computer = model.decoding.decoding.decoding_computer
|
|
if hasattr(decoding_computer, "reset_cuda_graphs_state"):
|
|
decoding_computer.reset_cuda_graphs_state()
|
|
|
|
|
|
def _decode_malsd_encoder_in_chunks(
|
|
decoding_computer,
|
|
encoder_output: torch.Tensor,
|
|
encoder_output_len: torch.Tensor,
|
|
chunk_size: int,
|
|
multi_biasing_ids: Optional[torch.Tensor] = None,
|
|
) -> BatchedBeamHyps:
|
|
encoder_output = encoder_output.transpose(1, 2)
|
|
state: Optional[BatchedBeamState] = None
|
|
current_batched_hyps: BatchedBeamHyps | None = None
|
|
decode_kwargs = {}
|
|
if multi_biasing_ids is not None:
|
|
decode_kwargs["multi_biasing_ids"] = multi_biasing_ids
|
|
|
|
for t in range(0, encoder_output.shape[1], chunk_size):
|
|
rest_len = encoder_output_len - t
|
|
current_len = torch.full_like(encoder_output_len, fill_value=chunk_size)
|
|
current_len = torch.minimum(current_len, rest_len)
|
|
current_len = torch.maximum(current_len, torch.zeros_like(current_len))
|
|
chunk_batched_hyps, state = decoding_computer(
|
|
x=encoder_output[:, t : t + chunk_size],
|
|
out_len=current_len,
|
|
prev_batched_state=state,
|
|
**decode_kwargs,
|
|
)
|
|
chunk_root_ptrs = chunk_batched_hyps.flatten_()
|
|
if current_batched_hyps is None:
|
|
current_batched_hyps = chunk_batched_hyps
|
|
else:
|
|
current_batched_hyps.merge_(
|
|
chunk_batched_hyps,
|
|
is_chunk_continuation=True,
|
|
boundary_prev_ptr=chunk_root_ptrs,
|
|
)
|
|
|
|
assert current_batched_hyps is not None
|
|
return current_batched_hyps
|
|
|
|
|
|
def _register_per_stream_biasing(
|
|
decoding_computer,
|
|
tokenizer,
|
|
boost_texts: list[str],
|
|
device: torch.device,
|
|
boosting_model_alpha: float = 10.0,
|
|
) -> tuple[torch.Tensor, list[BiasingRequestItemConfig | None]]:
|
|
batch_size = len(boost_texts)
|
|
multi_biasing_ids = torch.full([batch_size], fill_value=-1, dtype=torch.long, device=device)
|
|
biasing_requests: list[BiasingRequestItemConfig | None] = []
|
|
|
|
for batch_idx, boost_text in enumerate(boost_texts):
|
|
if not boost_text:
|
|
biasing_requests.append(None)
|
|
continue
|
|
request = BiasingRequestItemConfig(
|
|
boosting_model_cfg=BoostingTreeModelConfig(key_phrases_list=[boost_text], unk_score=-100),
|
|
boosting_model_alpha=boosting_model_alpha,
|
|
)
|
|
request.add_to_multi_model(
|
|
tokenizer=tokenizer,
|
|
biasing_multi_model=decoding_computer.biasing_multi_model,
|
|
)
|
|
if request.multi_model_id is not None:
|
|
multi_biasing_ids[batch_idx] = request.multi_model_id
|
|
biasing_requests.append(request)
|
|
|
|
return multi_biasing_ids, biasing_requests
|
|
|
|
|
|
def _unregister_per_stream_biasing(decoding_computer, biasing_requests: list[BiasingRequestItemConfig | None]) -> None:
|
|
for request in biasing_requests:
|
|
if request is not None and request.multi_model_id is not None:
|
|
decoding_computer.biasing_multi_model.remove_model(request.multi_model_id)
|
|
request.multi_model_id = None
|
|
|
|
|
|
def _run_malsd_streaming_manifest(
|
|
model: ASRModel,
|
|
manifest_path,
|
|
device: torch.device,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
boost_texts: Optional[list[str]] = None,
|
|
boosting_model_alpha: float = 10.0,
|
|
) -> list[str]:
|
|
manifest = read_manifest(manifest_path)
|
|
decoding_computer = model.decoding.decoding.decoding_computer
|
|
all_transcripts: list[str] = []
|
|
|
|
with torch.no_grad(), torch.inference_mode():
|
|
for i in range(0, len(manifest), batch_size):
|
|
batch_records = manifest[i : i + batch_size]
|
|
encoder_output, encoder_output_len = get_batch_encoder_outputs_from_records(
|
|
batch_records, model=model, device=device
|
|
)
|
|
|
|
multi_biasing_ids = None
|
|
biasing_requests: list[BiasingRequestItemConfig | None] = []
|
|
if boost_texts is not None:
|
|
assert decoding_computer.biasing_multi_model is not None
|
|
batch_boost_texts = boost_texts[i : i + batch_size]
|
|
multi_biasing_ids, biasing_requests = _register_per_stream_biasing(
|
|
decoding_computer,
|
|
model.tokenizer,
|
|
batch_boost_texts,
|
|
device,
|
|
boosting_model_alpha=boosting_model_alpha,
|
|
)
|
|
|
|
batched_hyps = _decode_malsd_encoder_in_chunks(
|
|
decoding_computer,
|
|
encoder_output,
|
|
encoder_output_len,
|
|
chunk_size,
|
|
multi_biasing_ids=multi_biasing_ids,
|
|
)
|
|
if boost_texts is not None:
|
|
_unregister_per_stream_biasing(decoding_computer, biasing_requests)
|
|
|
|
all_transcripts.extend(
|
|
model.tokenizer.ids_to_text(hyp.y_sequence.tolist())
|
|
for hyp in batched_hyps.to_hyps_list(score_norm=True)
|
|
)
|
|
|
|
return all_transcripts
|
|
|
|
|
|
def _run_streaming_batched_state(
|
|
model: ASRModel,
|
|
manifest_path,
|
|
device: torch.device,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
) -> tuple[list[str], list[str]]:
|
|
"""Drive the model's beam-search ``decoding_computer`` chunk-by-chunk and return
|
|
``(ref_transcripts, streaming_transcripts)``.
|
|
|
|
Shared between the MALSD and MAES streaming tests: both decoders return a
|
|
``(BatchedBeamHyps, None, BatchedBeamState)`` triple and accept
|
|
``prev_batched_state`` for cross-chunk state threading. The per-chunk results are
|
|
flattened and merged into a single accumulator via ``flatten_()`` +
|
|
``merge_(..., is_chunk_continuation=True, boundary_prev_ptr=...)`` -- the same
|
|
accumulation pattern used by the cache-aware / chunked streaming inference scripts.
|
|
"""
|
|
manifest = read_manifest(manifest_path)
|
|
transcriptions = model.transcribe(audio=str(manifest_path.absolute()), batch_size=batch_size)
|
|
ref_transcripts = [hyp.text for hyp in transcriptions]
|
|
|
|
all_hyps = []
|
|
decoding_computer = model.decoding.decoding.decoding_computer
|
|
with torch.no_grad(), torch.inference_mode():
|
|
for i in range(0, len(manifest), batch_size):
|
|
encoder_output, encoder_output_len = get_batch_encoder_outputs_from_records(
|
|
manifest[i : i + batch_size], model=model, device=device
|
|
)
|
|
state: Optional[BatchedBeamState] = None
|
|
current_batched_hyps: BatchedBeamHyps | None = None
|
|
encoder_output = encoder_output.transpose(1, 2) # (B, T, D)
|
|
for t in range(0, encoder_output.shape[1], chunk_size):
|
|
rest_len = encoder_output_len - t
|
|
current_len = torch.full_like(encoder_output_len, fill_value=chunk_size)
|
|
current_len = torch.minimum(current_len, rest_len)
|
|
current_len = torch.maximum(current_len, torch.zeros_like(current_len))
|
|
chunk_batched_hyps, state = decoding_computer(
|
|
x=encoder_output[:, t : t + chunk_size],
|
|
out_len=current_len,
|
|
prev_batched_state=state,
|
|
)
|
|
# Flatten this chunk's prefix tree and thread the cross-chunk beam
|
|
# permutation (``root_ptrs``) into the accumulator so the final
|
|
# ``flatten_sort_`` walks back through the right beam history.
|
|
chunk_root_ptrs = chunk_batched_hyps.flatten_()
|
|
if current_batched_hyps is None:
|
|
current_batched_hyps = chunk_batched_hyps
|
|
else:
|
|
current_batched_hyps.merge_(
|
|
chunk_batched_hyps,
|
|
is_chunk_continuation=True,
|
|
boundary_prev_ptr=chunk_root_ptrs,
|
|
)
|
|
assert current_batched_hyps is not None
|
|
# ``to_hyps_list`` mutates the prefix tree via ``flatten_sort_``, but we're done
|
|
# with ``current_batched_hyps`` here so an in-place call is fine.
|
|
all_hyps.extend(current_batched_hyps.to_hyps_list(score_norm=True))
|
|
|
|
streaming_transcripts = [model.tokenizer.ids_to_text(hyp.y_sequence.tolist()) for hyp in all_hyps]
|
|
return ref_transcripts, streaming_transcripts
|
|
|
|
|
|
@pytest.mark.with_downloads
|
|
@pytest.mark.parametrize("device,cuda_graphs_mode", DEVICE_PARAM_MATRIX)
|
|
@pytest.mark.parametrize("is_tdt", [False, True])
|
|
@pytest.mark.parametrize("chunk_size", [1, 3])
|
|
@pytest.mark.parametrize("batch_size", [4])
|
|
@pytest.mark.parametrize("beam_size", [4])
|
|
@pytest.mark.parametrize("max_symbols", [10])
|
|
def test_malsd_streaming_batched_state(
|
|
an4_val_manifest_corrected,
|
|
stt_en_fastconformer_transducer_large,
|
|
stt_en_fastconformer_tdt_large,
|
|
device: torch.device,
|
|
cuda_graphs_mode: Optional[str],
|
|
is_tdt: bool,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
beam_size: int,
|
|
max_symbols: int,
|
|
):
|
|
"""Streaming MALSD decoding with batched beam state passed across chunks."""
|
|
model = stt_en_fastconformer_tdt_large if is_tdt else stt_en_fastconformer_transducer_large
|
|
model.eval()
|
|
model.to(device=device)
|
|
|
|
_configure_malsd_decoding(model, cuda_graphs_mode, beam_size=beam_size, max_symbols=max_symbols)
|
|
|
|
ref_transcripts, streaming_transcripts = _run_streaming_batched_state(
|
|
model=model,
|
|
manifest_path=an4_val_manifest_corrected,
|
|
device=device,
|
|
chunk_size=chunk_size,
|
|
batch_size=batch_size,
|
|
)
|
|
assert ref_transcripts == streaming_transcripts
|
|
|
|
|
|
@pytest.mark.with_downloads
|
|
@pytest.mark.parametrize("device", MAES_DEVICE_PARAM_MATRIX)
|
|
@pytest.mark.parametrize("chunk_size", [1, 3])
|
|
@pytest.mark.parametrize("batch_size", [4])
|
|
@pytest.mark.parametrize("beam_size", [4])
|
|
@pytest.mark.parametrize("maes_num_steps", [2])
|
|
@pytest.mark.parametrize("maes_expansion_beta", [2])
|
|
@pytest.mark.parametrize("maes_expansion_gamma", [2.3])
|
|
def test_maes_streaming_batched_state(
|
|
an4_val_manifest_corrected,
|
|
stt_en_fastconformer_transducer_large,
|
|
device: torch.device,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
beam_size: int,
|
|
maes_num_steps: int,
|
|
maes_expansion_beta: int,
|
|
maes_expansion_gamma: float,
|
|
):
|
|
"""Streaming MAES decoding with batched beam state passed across chunks.
|
|
|
|
MAES is RNNT-only and pure-PyTorch (no CUDA graphs path), so the device matrix is
|
|
just the set of available devices and there is no ``cuda_graphs_mode`` / ``is_tdt``
|
|
parameter.
|
|
"""
|
|
model = stt_en_fastconformer_transducer_large
|
|
model.eval()
|
|
model.to(device=device)
|
|
|
|
_configure_maes_decoding(
|
|
model,
|
|
beam_size=beam_size,
|
|
maes_num_steps=maes_num_steps,
|
|
maes_expansion_beta=maes_expansion_beta,
|
|
maes_expansion_gamma=maes_expansion_gamma,
|
|
)
|
|
|
|
ref_transcripts, streaming_transcripts = _run_streaming_batched_state(
|
|
model=model,
|
|
manifest_path=an4_val_manifest_corrected,
|
|
device=device,
|
|
chunk_size=chunk_size,
|
|
batch_size=batch_size,
|
|
)
|
|
assert ref_transcripts == streaming_transcripts
|
|
|
|
|
|
# Phrases chosen from the AN4 val transcripts so the boosting tree is actually exercised
|
|
# (an empty / unseen list collapses to the no-boosting path and would not test fusion-state
|
|
# restoration across chunks).
|
|
_WB_KEY_PHRASES: list[str] = ["nineteen", "forty", "fifty", "repeat", "stop", "yes"]
|
|
|
|
|
|
@pytest.mark.with_downloads
|
|
@pytest.mark.parametrize("device,cuda_graphs_mode", DEVICE_PARAM_MATRIX)
|
|
@pytest.mark.parametrize("is_tdt", [False, True])
|
|
@pytest.mark.parametrize("chunk_size", [1, 3])
|
|
@pytest.mark.parametrize("batch_size", [4])
|
|
@pytest.mark.parametrize("beam_size", [4])
|
|
@pytest.mark.parametrize("max_symbols", [10])
|
|
def test_malsd_streaming_batched_state_with_word_boosting(
|
|
an4_val_manifest_corrected,
|
|
stt_en_fastconformer_transducer_large,
|
|
stt_en_fastconformer_tdt_large,
|
|
device: torch.device,
|
|
cuda_graphs_mode: Optional[str],
|
|
is_tdt: bool,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
beam_size: int,
|
|
max_symbols: int,
|
|
):
|
|
"""Streaming MALSD with word-boosting (``boosting_tree``) is chunk-invariant.
|
|
|
|
Adds a ``GPUBoostingTreeModel`` fusion model on top of the standard streaming MALSD
|
|
test. The reference (``model.transcribe``) and the streaming path are configured
|
|
identically, so the boosting tree's per-beam fusion states must be restored across
|
|
chunks via ``_init_decoding_state`` for the two transcripts to match.
|
|
"""
|
|
model = stt_en_fastconformer_tdt_large if is_tdt else stt_en_fastconformer_transducer_large
|
|
model.eval()
|
|
model.to(device=device)
|
|
|
|
_configure_malsd_decoding(
|
|
model,
|
|
cuda_graphs_mode,
|
|
beam_size=beam_size,
|
|
max_symbols=max_symbols,
|
|
key_phrases_list=_WB_KEY_PHRASES,
|
|
)
|
|
|
|
ref_transcripts, streaming_transcripts = _run_streaming_batched_state(
|
|
model=model,
|
|
manifest_path=an4_val_manifest_corrected,
|
|
device=device,
|
|
chunk_size=chunk_size,
|
|
batch_size=batch_size,
|
|
)
|
|
assert ref_transcripts == streaming_transcripts
|
|
|
|
|
|
@pytest.mark.with_downloads
|
|
@pytest.mark.parametrize("device,cuda_graphs_mode", DEVICE_PARAM_MATRIX)
|
|
@pytest.mark.parametrize("is_tdt", [False, True])
|
|
@pytest.mark.parametrize("chunk_size", [1])
|
|
@pytest.mark.parametrize("batch_size", [4])
|
|
@pytest.mark.parametrize("beam_size", [4])
|
|
@pytest.mark.parametrize("max_symbols", [10])
|
|
def test_malsd_streaming_boosting_with_ref_transcripts(
|
|
an4_val_manifest_corrected,
|
|
stt_en_fastconformer_transducer_large,
|
|
stt_en_fastconformer_tdt_large,
|
|
device: torch.device,
|
|
cuda_graphs_mode: Optional[str],
|
|
is_tdt: bool,
|
|
chunk_size: int,
|
|
batch_size: int,
|
|
beam_size: int,
|
|
max_symbols: int,
|
|
):
|
|
"""Metamorphic test analogous to ``test_label_looping_streaming_boosting_with_ref_transcripts``."""
|
|
model = stt_en_fastconformer_tdt_large if is_tdt else stt_en_fastconformer_transducer_large
|
|
model.eval()
|
|
model.to(device=device)
|
|
|
|
_configure_malsd_decoding(model, cuda_graphs_mode, beam_size, max_symbols)
|
|
ref_transcripts = [
|
|
hyp.text for hyp in model.transcribe(audio=str(an4_val_manifest_corrected.absolute()), batch_size=batch_size)
|
|
]
|
|
|
|
_configure_malsd_decoding(model, cuda_graphs_mode, beam_size, max_symbols, enable_per_stream_biasing=True)
|
|
_reset_decoding_computer_state(model)
|
|
|
|
streaming_transcripts = _run_malsd_streaming_manifest(
|
|
model,
|
|
an4_val_manifest_corrected,
|
|
device,
|
|
chunk_size,
|
|
batch_size,
|
|
boost_texts=ref_transcripts,
|
|
)
|
|
assert ref_transcripts == streaming_transcripts
|