ba4be087d5
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
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
798 lines
37 KiB
Python
798 lines
37 KiB
Python
# Copyright (c) 2023, 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.
|
|
|
|
from typing import List
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from nemo.collections.asr.parts.submodules.transducer_decoding.batched_hyps import BatchedHyps
|
|
from nemo.collections.asr.parts.utils.rnnt_utils import batched_hyps_to_hypotheses
|
|
from tests.collections.asr.decoding.utils import avoid_sync_operations
|
|
|
|
DEVICES: List[torch.device] = [torch.device("cpu")]
|
|
|
|
if torch.cuda.is_available():
|
|
DEVICES.append(torch.device("cuda"))
|
|
|
|
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
DEVICES.append(torch.device("mps"))
|
|
|
|
# blank id that does not collide with any non-blank label used in the "no blank steps" tests
|
|
NON_COLLIDING_BLANK_ID = 1024
|
|
|
|
|
|
class TestBatchedHyps:
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_instantiate(self, device: torch.device):
|
|
hyps = BatchedHyps(batch_size=2, init_length=3, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
assert torch.is_tensor(hyps.timestamps)
|
|
# device: for mps device we need to use `type`, not directly compare
|
|
assert hyps.timestamps.device.type == device.type
|
|
assert hyps.timestamps.shape == (2, 3)
|
|
assert hyps.transcript.shape == (2, 3)
|
|
assert hyps.scores.shape == (2,)
|
|
assert hyps.current_lengths.tolist() == [0, 0]
|
|
# optional storage is disabled by default
|
|
assert hyps.token_durations is None
|
|
assert hyps.step_confidence is None
|
|
assert hyps.logits is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("batch_size", [-1, 0])
|
|
def test_instantiate_incorrect_batch_size(self, batch_size):
|
|
with pytest.raises(ValueError):
|
|
_ = BatchedHyps(batch_size=batch_size, init_length=3, blank_id=0)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("init_length", [-1, 0])
|
|
def test_instantiate_incorrect_init_length(self, init_length):
|
|
with pytest.raises(ValueError):
|
|
_ = BatchedHyps(batch_size=1, init_length=init_length, blank_id=0)
|
|
|
|
@pytest.mark.unit
|
|
def test_instantiate_with_logits_requires_logits_dim(self):
|
|
# `with_logits=True` without `logits_dim` is invalid
|
|
with pytest.raises(ValueError):
|
|
_ = BatchedHyps(batch_size=1, init_length=3, blank_id=0, with_logits=True, logits_dim=None)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_instantiate_optional_storage(self, device: torch.device):
|
|
logits_dim = 7
|
|
hyps = BatchedHyps(
|
|
batch_size=2,
|
|
init_length=3,
|
|
blank_id=0,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
with_durations=True,
|
|
with_step_confidence=True,
|
|
with_duration_confidence=True,
|
|
with_logits=True,
|
|
)
|
|
assert hyps.token_durations.shape == (2, 3)
|
|
# duration confidence makes the confidence tensor store a pair (step + duration) per token
|
|
assert hyps.step_confidence.shape == (2, 3, 2)
|
|
assert hyps.logits.shape == (2, 3, logits_dim)
|
|
|
|
# without duration confidence the confidence tensor is 2d
|
|
hyps_no_dur_conf = BatchedHyps(
|
|
batch_size=2,
|
|
init_length=3,
|
|
blank_id=0,
|
|
device=device,
|
|
with_step_confidence=True,
|
|
with_duration_confidence=False,
|
|
)
|
|
assert hyps_no_dur_conf.step_confidence.shape == (2, 3)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_add_results_masked(self, device: torch.device):
|
|
# batch of size 2, add label for first utterance only (second is inactive)
|
|
hyps = BatchedHyps(batch_size=2, init_length=1, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([5, 1], device=device),
|
|
time_indices=torch.tensor([1, 0], device=device),
|
|
scores=torch.tensor([0.5, 10.0], device=device),
|
|
)
|
|
assert hyps.current_lengths.tolist() == [1, 0]
|
|
assert hyps.transcript.tolist()[0][:1] == [5]
|
|
assert hyps.timestamps.tolist()[0][:1] == [1]
|
|
assert hyps.scores.tolist() == pytest.approx([0.5, 0.0]) # inactive score should be ignored!
|
|
assert hyps.last_nb_timestamp.tolist() == [1, -1]
|
|
assert hyps.last_nb_timestamp_lasts.tolist() == [1, 0]
|
|
assert hyps.last_nb_labels.tolist() == [5, -1]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_add_multiple_results_masked(self, device: torch.device):
|
|
# batch of size 2, add label for first utterance, then add labels for both utterances
|
|
hyps = BatchedHyps(batch_size=2, init_length=1, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([5, 2], device=device),
|
|
time_indices=torch.tensor([1, 0], device=device),
|
|
scores=torch.tensor([0.5, 10.0], device=device),
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([2, 4], device=device),
|
|
time_indices=torch.tensor([1, 2], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
)
|
|
assert hyps.current_lengths.tolist() == [2, 1]
|
|
assert hyps.transcript.tolist()[0][:2] == [5, 2]
|
|
assert hyps.transcript.tolist()[1][:1] == [4]
|
|
assert hyps.timestamps.tolist()[0][:2] == [1, 1]
|
|
assert hyps.timestamps.tolist()[1][:1] == [2]
|
|
assert hyps.scores.tolist() == pytest.approx([1.5, 1.0])
|
|
assert hyps.last_nb_timestamp.tolist() == [1, 2]
|
|
assert hyps.last_nb_timestamp_lasts.tolist() == [2, 1]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_add_results_masked_no_checks(self, device: torch.device):
|
|
# `check_lengths=False` must contain no host<->device synchronization (blocking) operations
|
|
hyps = BatchedHyps(batch_size=2, init_length=4, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
active_mask = torch.tensor([True, False], device=device)
|
|
time_indices = torch.tensor([1, 0], device=device)
|
|
scores = torch.tensor([0.5, 10.0], device=device)
|
|
labels = torch.tensor([5, 1], device=device)
|
|
# check there are no blocking operations
|
|
with avoid_sync_operations(device=device):
|
|
hyps.add_results_masked_(
|
|
active_mask=active_mask,
|
|
labels=labels,
|
|
time_indices=time_indices,
|
|
scores=scores,
|
|
check_lengths=False,
|
|
)
|
|
assert hyps.current_lengths.tolist() == [1, 0]
|
|
assert hyps.transcript.tolist()[0][:1] == [5]
|
|
assert hyps.timestamps.tolist()[0][:1] == [1]
|
|
assert hyps.scores.tolist() == pytest.approx([0.5, 0.0]) # inactive score should be ignored!
|
|
assert hyps.last_nb_timestamp.tolist() == [1, -1]
|
|
assert hyps.last_nb_timestamp_lasts.tolist() == [1, 0]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_add_results_masked_reallocates(self, device: torch.device):
|
|
# init_length is intentionally small; storage must grow transparently when check_lengths=True
|
|
hyps = BatchedHyps(batch_size=2, init_length=1, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
for step in range(5):
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([step, step + 10], device=device),
|
|
time_indices=torch.tensor([step, step], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
check_lengths=True,
|
|
)
|
|
assert hyps._max_length >= 5
|
|
assert hyps.current_lengths.tolist() == [5, 5]
|
|
assert hyps.transcript.tolist()[0][:5] == [0, 1, 2, 3, 4]
|
|
assert hyps.transcript.tolist()[1][:5] == [10, 11, 12, 13, 14]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_add_results_masked_with_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=True: blank labels are stored in the transcript, but they do NOT advance
|
|
# the score / last-non-blank tracking. Single utterance for clarity.
|
|
blank_id = 0
|
|
hyps = BatchedHyps(batch_size=1, init_length=2, blank_id=blank_id, device=device, with_blank_steps=True)
|
|
# (label, time, score): two non-blank tokens at t=0, then blank, then non-blank at t=1, then blank
|
|
steps = [
|
|
(3, 0, 1.0), # non-blank
|
|
(5, 0, 1.5), # non-blank, same timestamp
|
|
(blank_id, 0, 0.1), # blank
|
|
(7, 1, 2.0), # non-blank
|
|
(blank_id, 1, 0.2), # blank
|
|
]
|
|
for label, time, score in steps:
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([label], device=device),
|
|
time_indices=torch.tensor([time], device=device),
|
|
scores=torch.tensor([score], device=device),
|
|
)
|
|
# all steps (including blanks) are stored
|
|
assert hyps.current_lengths.tolist() == [5]
|
|
assert hyps.transcript.tolist()[0][:5] == [3, 5, blank_id, 7, blank_id]
|
|
assert hyps.timestamps.tolist()[0][:5] == [0, 0, 0, 1, 1]
|
|
# only non-blank scores accumulate: 1.0 + 1.5 + 2.0
|
|
assert hyps.scores.tolist() == pytest.approx([4.5])
|
|
# last-non-blank tracking ignores blanks
|
|
assert hyps.last_nb_timestamp.tolist() == [1]
|
|
assert hyps.last_nb_timestamp_lasts.tolist() == [1]
|
|
assert hyps.last_nb_labels.tolist() == [7]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_get_data_without_blank_no_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=False: data is returned as-is (it never contained blanks)
|
|
hyps = BatchedHyps(batch_size=2, init_length=2, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([5, 4], device=device),
|
|
time_indices=torch.tensor([0, 1], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([2, 0], device=device),
|
|
time_indices=torch.tensor([1, 0], device=device),
|
|
scores=torch.tensor([1.0, 0.0], device=device),
|
|
)
|
|
lengths, transcript, timestamps, durations, confidence = hyps.get_data_without_blank()
|
|
# returned objects are the underlying (unmodified) tensors
|
|
assert lengths is hyps.current_lengths
|
|
assert transcript is hyps.transcript
|
|
assert timestamps is hyps.timestamps
|
|
assert durations is None
|
|
assert confidence is None
|
|
assert lengths.tolist() == [2, 1]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_get_data_without_blank_with_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=True: blanks are stripped and non-blank order is preserved
|
|
blank_id = 0
|
|
hyps = BatchedHyps(batch_size=2, init_length=2, blank_id=blank_id, device=device, with_blank_steps=True)
|
|
# seq 0: [5, blank, 2, blank] -> [5, 2]
|
|
# seq 1: [blank, 4] -> [4]
|
|
steps = [
|
|
# (labels, times, active_mask)
|
|
([5, blank_id], [0, 0], [True, True]),
|
|
([blank_id, 4], [0, 1], [True, True]),
|
|
([2, blank_id], [1, 1], [True, False]),
|
|
([blank_id, 0], [1, 0], [True, False]),
|
|
]
|
|
for labels, times, active in steps:
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor(active, device=device),
|
|
labels=torch.tensor(labels, device=device),
|
|
time_indices=torch.tensor(times, device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
)
|
|
lengths, transcript, timestamps, durations, confidence = hyps.get_data_without_blank()
|
|
assert lengths.tolist() == [2, 1]
|
|
assert transcript[0, :2].tolist() == [5, 2]
|
|
assert transcript[1, :1].tolist() == [4]
|
|
assert timestamps[0, :2].tolist() == [0, 1]
|
|
assert timestamps[1, :1].tolist() == [1]
|
|
assert durations is None
|
|
assert confidence is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_get_last_labels(self, device: torch.device):
|
|
hyps = BatchedHyps(batch_size=2, init_length=2, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
# no labels yet -> pad_id everywhere
|
|
assert hyps.get_last_labels(pad_id=-1).tolist() == [-1, -1]
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([5, 1], device=device),
|
|
time_indices=torch.tensor([0, 0], device=device),
|
|
scores=torch.tensor([1.0, 0.0], device=device),
|
|
)
|
|
assert hyps.get_last_labels(pad_id=-1).tolist() == [5, -1]
|
|
assert hyps.get_last_labels(pad_id=100).tolist() == [5, 100]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_clear(self, device: torch.device):
|
|
hyps = BatchedHyps(batch_size=2, init_length=2, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([5, 4], device=device),
|
|
time_indices=torch.tensor([0, 0], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
)
|
|
hyps.clear_()
|
|
assert hyps.current_lengths.tolist() == [0, 0]
|
|
assert hyps.scores.tolist() == pytest.approx([0.0, 0.0])
|
|
assert hyps.transcript.tolist() == [[0, 0], [0, 0]]
|
|
assert hyps.last_nb_timestamp.tolist() == [-1, -1]
|
|
assert hyps.last_nb_timestamp_lasts.tolist() == [0, 0]
|
|
assert hyps.last_nb_labels.tolist() == [-1, -1]
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_clone(self, device: torch.device):
|
|
logits_dim = 7
|
|
hyps = BatchedHyps(
|
|
batch_size=2,
|
|
init_length=2,
|
|
blank_id=0,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
with_durations=True,
|
|
with_step_confidence=True,
|
|
with_logits=True,
|
|
with_blank_steps=True,
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([5, 4], device=device),
|
|
time_indices=torch.tensor([0, 0], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
token_durations=torch.tensor([1, 2], device=device),
|
|
confidence=torch.tensor([0.9, 0.8], device=device),
|
|
logits=torch.rand((2, logits_dim), device=device),
|
|
)
|
|
clone = hyps.clone()
|
|
# flags carried over
|
|
assert clone.with_durations and clone.with_step_confidence and clone.with_logits and clone.with_blank_steps
|
|
assert clone.blank_id == hyps.blank_id
|
|
# values copied
|
|
assert clone.current_lengths.tolist() == hyps.current_lengths.tolist()
|
|
assert torch.equal(clone.transcript, hyps.transcript)
|
|
assert torch.allclose(clone.logits, hyps.logits)
|
|
assert torch.allclose(clone.step_confidence, hyps.step_confidence)
|
|
assert torch.equal(clone.token_durations, hyps.token_durations)
|
|
# clone is independent of the original
|
|
hyps.transcript.fill_(0)
|
|
hyps.scores.fill_(0.0)
|
|
assert clone.transcript.tolist()[0][:1] == [5]
|
|
assert clone.scores.tolist() == pytest.approx([1.0, 1.0])
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_merge(self, device: torch.device):
|
|
# merge two batched hypotheses (basic case: no blank steps, no optional storage)
|
|
def build(labels_per_step, times_per_step, masks_per_step, scores_per_step):
|
|
hyps = BatchedHyps(batch_size=2, init_length=4, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
for labels, times, mask, scores in zip(labels_per_step, times_per_step, masks_per_step, scores_per_step):
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor(mask, device=device),
|
|
labels=torch.tensor(labels, device=device),
|
|
time_indices=torch.tensor(times, device=device),
|
|
scores=torch.tensor(scores, device=device),
|
|
)
|
|
return hyps
|
|
|
|
# A: seq0=[5, 2], seq1=[4]
|
|
hyps_a = build(
|
|
labels_per_step=[[5, 4], [2, 0]],
|
|
times_per_step=[[0, 0], [1, 0]],
|
|
masks_per_step=[[True, True], [True, False]],
|
|
scores_per_step=[[0.5, 0.7], [0.5, 0.0]],
|
|
)
|
|
# B: seq0=[7], seq1=[8, 9]
|
|
hyps_b = build(
|
|
labels_per_step=[[7, 8], [0, 9]],
|
|
times_per_step=[[2, 2], [0, 3]],
|
|
masks_per_step=[[True, True], [False, True]],
|
|
scores_per_step=[[0.3, 0.3], [0.0, 0.4]],
|
|
)
|
|
|
|
hyps_a.merge_(hyps_b)
|
|
assert hyps_a.current_lengths.tolist() == [3, 3]
|
|
assert hyps_a.transcript[0, :3].tolist() == [5, 2, 7]
|
|
assert hyps_a.transcript[1, :3].tolist() == [4, 8, 9]
|
|
assert hyps_a.scores.tolist() == pytest.approx([1.3, 1.4])
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_merge_with_logits(self, device: torch.device):
|
|
# Regression test: merge_ must use dim=1 (sequence axis) for logits scatter/cat,
|
|
# not dim=-1 (logits-dim axis), which would silently corrupt the data.
|
|
logits_dim = 5
|
|
blank_id = NON_COLLIDING_BLANK_ID
|
|
|
|
def build_with_logits(labels_per_step, times_per_step, masks_per_step, scores_per_step, logits_per_step):
|
|
hyps = BatchedHyps(
|
|
batch_size=2,
|
|
init_length=4,
|
|
blank_id=blank_id,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
float_dtype=torch.float32,
|
|
with_logits=True,
|
|
)
|
|
for labels, times, mask, scores, logits in zip(
|
|
labels_per_step, times_per_step, masks_per_step, scores_per_step, logits_per_step
|
|
):
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor(mask, device=device),
|
|
labels=torch.tensor(labels, device=device),
|
|
time_indices=torch.tensor(times, device=device),
|
|
scores=torch.tensor(scores, device=device),
|
|
logits=logits,
|
|
)
|
|
return hyps
|
|
|
|
# Fixed logits so we can assert exact values after merge
|
|
logits_a0 = torch.full((2, logits_dim), 1.0, device=device) # step for seq0=[5], seq1=[4]
|
|
logits_a1 = torch.full((2, logits_dim), 2.0, device=device) # step for seq0=[2], seq1 inactive
|
|
|
|
logits_b0 = torch.full((2, logits_dim), 3.0, device=device) # step for seq0=[7], seq1=[8]
|
|
logits_b1 = torch.full((2, logits_dim), 4.0, device=device) # step for seq0 inactive, seq1=[9]
|
|
|
|
# A: seq0=[5, 2], seq1=[4]
|
|
hyps_a = build_with_logits(
|
|
labels_per_step=[[5, 4], [2, 0]],
|
|
times_per_step=[[0, 0], [1, 0]],
|
|
masks_per_step=[[True, True], [True, False]],
|
|
scores_per_step=[[0.5, 0.7], [0.5, 0.0]],
|
|
logits_per_step=[logits_a0, logits_a1],
|
|
)
|
|
# B: seq0=[7], seq1=[8, 9]
|
|
hyps_b = build_with_logits(
|
|
labels_per_step=[[7, 8], [0, 9]],
|
|
times_per_step=[[2, 2], [0, 3]],
|
|
masks_per_step=[[True, True], [False, True]],
|
|
scores_per_step=[[0.3, 0.3], [0.0, 0.4]],
|
|
logits_per_step=[logits_b0, logits_b1],
|
|
)
|
|
|
|
hyps_a.merge_(hyps_b)
|
|
|
|
# Sequence lengths: seq0=2+1=3, seq1=1+2=3
|
|
assert hyps_a.current_lengths.tolist() == [3, 3]
|
|
|
|
# seq0 logits: positions [0,1] from A (value=1 then 2), position [2] from B (value=3)
|
|
assert torch.allclose(hyps_a.logits[0, 0], torch.full((logits_dim,), 1.0, device=device))
|
|
assert torch.allclose(hyps_a.logits[0, 1], torch.full((logits_dim,), 2.0, device=device))
|
|
assert torch.allclose(hyps_a.logits[0, 2], torch.full((logits_dim,), 3.0, device=device))
|
|
|
|
# seq1 logits: position [0] from A (value=1), positions [1,2] from B (value=3 then 4)
|
|
assert torch.allclose(hyps_a.logits[1, 0], torch.full((logits_dim,), 1.0, device=device))
|
|
assert torch.allclose(hyps_a.logits[1, 1], torch.full((logits_dim,), 3.0, device=device))
|
|
assert torch.allclose(hyps_a.logits[1, 2], torch.full((logits_dim,), 4.0, device=device))
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_merge_with_logits_triggers_reallocation(self, device: torch.device):
|
|
# When the combined length exceeds _max_length, merge_ must reallocate logits along dim=1
|
|
# (sequence axis). With the dim=-1 bug, the reallocation would expand the logits-dim
|
|
# axis instead, causing a shape mismatch on the subsequent scatter_.
|
|
logits_dim = 5
|
|
blank_id = NON_COLLIDING_BLANK_ID
|
|
|
|
# Use init_length=1 to force reallocation during merge
|
|
hyps_a = BatchedHyps(
|
|
batch_size=1,
|
|
init_length=1,
|
|
blank_id=blank_id,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
float_dtype=torch.float32,
|
|
with_logits=True,
|
|
)
|
|
hyps_b = BatchedHyps(
|
|
batch_size=1,
|
|
init_length=1,
|
|
blank_id=blank_id,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
float_dtype=torch.float32,
|
|
with_logits=True,
|
|
)
|
|
|
|
logits_a = torch.full((1, logits_dim), 1.0, device=device)
|
|
logits_b = torch.full((1, logits_dim), 2.0, device=device)
|
|
|
|
hyps_a.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([5], device=device),
|
|
time_indices=torch.tensor([0], device=device),
|
|
scores=torch.tensor([1.0], device=device),
|
|
logits=logits_a,
|
|
)
|
|
hyps_b.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([7], device=device),
|
|
time_indices=torch.tensor([1], device=device),
|
|
scores=torch.tensor([1.0], device=device),
|
|
logits=logits_b,
|
|
)
|
|
|
|
# cur_len=1, other_len=1 -> combined=2 >= init_length=1, so reallocation is triggered
|
|
hyps_a.merge_(hyps_b)
|
|
|
|
assert hyps_a.current_lengths.tolist() == [2]
|
|
assert hyps_a.logits.shape == (1, hyps_a._max_length, logits_dim)
|
|
assert torch.allclose(hyps_a.logits[0, 0], torch.full((logits_dim,), 1.0, device=device))
|
|
assert torch.allclose(hyps_a.logits[0, 1], torch.full((logits_dim,), 2.0, device=device))
|
|
|
|
|
|
class TestConvertToHypotheses:
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_no_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=False: transcript already contains only non-blank labels
|
|
hyps = BatchedHyps(batch_size=2, init_length=1, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([5, 0], device=device),
|
|
time_indices=torch.tensor([1, 0], device=device),
|
|
scores=torch.tensor([0.5, 0.0], device=device),
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([2, 4], device=device),
|
|
time_indices=torch.tensor([1, 2], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert (hypotheses[0].y_sequence == torch.tensor([5, 2], device="cpu")).all()
|
|
assert (hypotheses[1].y_sequence == torch.tensor([4], device="cpu")).all()
|
|
assert hypotheses[0].score == pytest.approx(1.5)
|
|
assert hypotheses[1].score == pytest.approx(1.0)
|
|
assert (hypotheses[0].timestamp == torch.tensor([1, 1], device="cpu")).all()
|
|
assert (hypotheses[1].timestamp == torch.tensor([2], device="cpu")).all()
|
|
# no blank steps -> no alignments / frame confidence
|
|
assert hypotheses[0].alignments is None
|
|
assert hypotheses[1].alignments is None
|
|
assert hypotheses[0].frame_confidence is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_batch_size_arg(self, device: torch.device):
|
|
# batch_size arg returns only the first `batch_size` hypotheses (CUDA-graph constant batch)
|
|
hyps = BatchedHyps(batch_size=4, init_length=1, blank_id=NON_COLLIDING_BLANK_ID, device=device)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True, True, True], device=device),
|
|
labels=torch.tensor([5, 4, 3, 2], device=device),
|
|
time_indices=torch.tensor([0, 0, 0, 0], device=device),
|
|
scores=torch.tensor([1.0, 1.0, 1.0, 1.0], device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps, batch_size=2)
|
|
assert len(hypotheses) == 2
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_with_blank_steps_strips_blanks(self, device: torch.device):
|
|
# with_blank_steps=True but no logits/confidence: blanks must be stripped from y_sequence/timestamps,
|
|
# while alignments are NOT produced (no logits recorded)
|
|
blank_id = 0
|
|
hyps = BatchedHyps(batch_size=2, init_length=2, blank_id=blank_id, device=device, with_blank_steps=True)
|
|
# seq 0: [5, blank, 2, blank] -> [5, 2]
|
|
# seq 1: [blank, 4] -> [4]
|
|
steps = [
|
|
([5, blank_id], [0, 0], [True, True], [0.5, 0.1]),
|
|
([blank_id, 4], [0, 1], [True, True], [0.1, 1.0]),
|
|
([2, blank_id], [1, 1], [True, False], [1.0, 0.0]),
|
|
([blank_id, 0], [1, 0], [True, False], [0.1, 0.0]),
|
|
]
|
|
for labels, times, active, scores in steps:
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor(active, device=device),
|
|
labels=torch.tensor(labels, device=device),
|
|
time_indices=torch.tensor(times, device=device),
|
|
scores=torch.tensor(scores, device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert (hypotheses[0].y_sequence == torch.tensor([5, 2], device="cpu")).all()
|
|
assert (hypotheses[1].y_sequence == torch.tensor([4], device="cpu")).all()
|
|
assert (hypotheses[0].timestamp == torch.tensor([0, 1], device="cpu")).all()
|
|
assert (hypotheses[1].timestamp == torch.tensor([1], device="cpu")).all()
|
|
# only non-blank scores accumulated
|
|
assert hypotheses[0].score == pytest.approx(1.5)
|
|
assert hypotheses[1].score == pytest.approx(1.0)
|
|
# no logits recorded -> alignments stay None even though blank steps were stored
|
|
assert hypotheses[0].alignments is None
|
|
assert hypotheses[1].alignments is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_logits_no_alignments_without_blank_steps(self, device: torch.device):
|
|
# logits are recorded but with_blank_steps=False -> alignments must NOT be produced
|
|
logits_dim = 7
|
|
hyps = BatchedHyps(
|
|
batch_size=2,
|
|
init_length=2,
|
|
blank_id=6,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
with_logits=True,
|
|
with_blank_steps=False,
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([5, 4], device=device),
|
|
time_indices=torch.tensor([0, 0], device=device),
|
|
scores=torch.tensor([1.0, 1.0], device=device),
|
|
logits=torch.rand((2, logits_dim), device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert hypotheses[0].alignments is None
|
|
assert hypotheses[1].alignments is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_with_blank_steps_and_logits_alignments(self, device: torch.device):
|
|
# Reproduces alignment recovery: with_blank_steps=True + with_logits=True
|
|
batch_size = 2
|
|
logits_dim = 7
|
|
blank_index = 6
|
|
hyps = BatchedHyps(
|
|
batch_size=batch_size,
|
|
init_length=1,
|
|
blank_id=blank_index,
|
|
logits_dim=logits_dim,
|
|
device=device,
|
|
with_logits=True,
|
|
with_blank_steps=True,
|
|
)
|
|
# sequence 0: [[5, blank], [2, blank]] -> [5, 2]
|
|
# sequence 1: [[blank ], [4, blank]] -> [4]
|
|
# one logits row per (batch, add-call); rows belonging to inactive entries are ignored
|
|
L = [torch.rand((batch_size, logits_dim), device=device) for _ in range(4)]
|
|
# call0: seq0=5@t0, seq1=blank@t0
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([5, blank_index], device=device),
|
|
time_indices=torch.tensor([0, 0], device=device),
|
|
scores=torch.tensor([0.5, 0.1], device=device),
|
|
logits=L[0],
|
|
)
|
|
# call1: seq0=blank@t0, seq1=4@t1
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([blank_index, 4], device=device),
|
|
time_indices=torch.tensor([0, 1], device=device),
|
|
scores=torch.tensor([0.1, 1.0], device=device),
|
|
logits=L[1],
|
|
)
|
|
# call2: seq0=2@t1, seq1=blank@t1
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, True], device=device),
|
|
labels=torch.tensor([2, blank_index], device=device),
|
|
time_indices=torch.tensor([1, 1], device=device),
|
|
scores=torch.tensor([1.0, 0.1], device=device),
|
|
logits=L[2],
|
|
)
|
|
# call3: seq0=blank@t1, seq1 inactive
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True, False], device=device),
|
|
labels=torch.tensor([blank_index, 0], device=device),
|
|
time_indices=torch.tensor([1, 0], device=device),
|
|
scores=torch.tensor([0.1, 0.0], device=device),
|
|
logits=L[3],
|
|
)
|
|
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert (hypotheses[0].y_sequence == torch.tensor([5, 2], device="cpu")).all()
|
|
assert (hypotheses[1].y_sequence == torch.tensor([4], device="cpu")).all()
|
|
assert hypotheses[0].score == pytest.approx(1.5)
|
|
assert hypotheses[1].score == pytest.approx(1.0)
|
|
assert (hypotheses[0].timestamp == torch.tensor([0, 1], device="cpu")).all()
|
|
assert (hypotheses[1].timestamp == torch.tensor([1], device="cpu")).all()
|
|
|
|
# alignments are grouped by timestamp; each entry is a (logits, label) tuple
|
|
etalon = [
|
|
[
|
|
[(L[0][0].cpu(), 5), (L[1][0].cpu(), blank_index)],
|
|
[(L[2][0].cpu(), 2), (L[3][0].cpu(), blank_index)],
|
|
],
|
|
[
|
|
[(L[0][1].cpu(), blank_index)],
|
|
[(L[1][1].cpu(), 4), (L[2][1].cpu(), blank_index)],
|
|
],
|
|
]
|
|
for batch_i in range(batch_size):
|
|
assert len(hypotheses[batch_i].alignments) == len(etalon[batch_i])
|
|
for t, group_for_timestamp in enumerate(etalon[batch_i]):
|
|
assert len(hypotheses[batch_i].alignments[t]) == len(group_for_timestamp)
|
|
for step, (current_logits, label) in enumerate(group_for_timestamp):
|
|
assert torch.allclose(hypotheses[batch_i].alignments[t][step][0], current_logits)
|
|
assert hypotheses[batch_i].alignments[t][step][1] == label
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_with_durations(self, device: torch.device):
|
|
# TDT-style: token durations are stored and (with blank steps) stripped together with blanks
|
|
blank_id = 0
|
|
hyps = BatchedHyps(
|
|
batch_size=1,
|
|
init_length=2,
|
|
blank_id=blank_id,
|
|
device=device,
|
|
with_durations=True,
|
|
with_blank_steps=True,
|
|
)
|
|
# transcript [3, blank, 7, blank] with durations [2, 1, 4, 1] -> [3, 7] with durations [2, 4]
|
|
steps = [
|
|
(3, 0, 2, 1.0),
|
|
(blank_id, 2, 1, 0.1),
|
|
(7, 3, 4, 2.0),
|
|
(blank_id, 7, 1, 0.2),
|
|
]
|
|
for label, time, duration, score in steps:
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([label], device=device),
|
|
time_indices=torch.tensor([time], device=device),
|
|
scores=torch.tensor([score], device=device),
|
|
token_durations=torch.tensor([duration], device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert (hypotheses[0].y_sequence == torch.tensor([3, 7], device="cpu")).all()
|
|
assert (hypotheses[0].timestamp == torch.tensor([0, 3], device="cpu")).all()
|
|
assert (hypotheses[0].token_duration == torch.tensor([2, 4], device="cpu")).all()
|
|
assert hypotheses[0].score == pytest.approx(3.0)
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_with_step_confidence_no_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=False: per-token confidence is precomputed, no frame_confidence (no blank steps)
|
|
hyps = BatchedHyps(
|
|
batch_size=1,
|
|
init_length=2,
|
|
blank_id=NON_COLLIDING_BLANK_ID,
|
|
device=device,
|
|
with_step_confidence=True,
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([5], device=device),
|
|
time_indices=torch.tensor([0], device=device),
|
|
scores=torch.tensor([1.0], device=device),
|
|
confidence=torch.tensor([0.9], device=device),
|
|
)
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([2], device=device),
|
|
time_indices=torch.tensor([1], device=device),
|
|
scores=torch.tensor([1.0], device=device),
|
|
confidence=torch.tensor([0.8], device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
assert hypotheses[0].non_blank_step_confidence_precomputed == pytest.approx([0.9, 0.8])
|
|
assert hypotheses[0].frame_confidence is None
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.parametrize("device", DEVICES)
|
|
def test_convert_with_step_confidence_and_blank_steps(self, device: torch.device):
|
|
# with_blank_steps=True: frame_confidence is grouped per timestamp (incl. blanks),
|
|
# while non_blank_step_confidence_precomputed holds only non-blank tokens
|
|
blank_id = 0
|
|
hyps = BatchedHyps(
|
|
batch_size=1,
|
|
init_length=2,
|
|
blank_id=blank_id,
|
|
device=device,
|
|
with_step_confidence=True,
|
|
with_blank_steps=True,
|
|
)
|
|
# transcript [3, blank, 7], confidence [0.9, 0.5, 0.8], timestamps [0, 0, 1]
|
|
steps = [
|
|
(3, 0, 0.9, 1.0),
|
|
(blank_id, 0, 0.5, 0.1),
|
|
(7, 1, 0.8, 2.0),
|
|
]
|
|
for label, time, confidence, score in steps:
|
|
hyps.add_results_masked_(
|
|
active_mask=torch.tensor([True], device=device),
|
|
labels=torch.tensor([label], device=device),
|
|
time_indices=torch.tensor([time], device=device),
|
|
scores=torch.tensor([score], device=device),
|
|
confidence=torch.tensor([confidence], device=device),
|
|
)
|
|
hypotheses = batched_hyps_to_hypotheses(hyps)
|
|
# non-blank tokens only
|
|
assert hypotheses[0].non_blank_step_confidence_precomputed == pytest.approx([0.9, 0.8])
|
|
# grouped by timestamp: t=0 has 2 steps (token + blank), t=1 has 1 step
|
|
frame_confidence = hypotheses[0].frame_confidence
|
|
assert len(frame_confidence) == 2
|
|
assert len(frame_confidence[0]) == 2
|
|
assert len(frame_confidence[1]) == 1
|
|
flat = [float(c) for group in frame_confidence for c in group]
|
|
assert flat == pytest.approx([0.9, 0.5, 0.8])
|