Skip to content

Commit ea2c046

Browse files
pzelaskotango4j
andauthored
feat(speechlm2): add generalized speech encoder foundations (#16192)
* feat(asr): add grouped expert encoder foundations Introduce heterogeneous Transformer grouping, MoE routing, the GGEMM Parallel Expert Encoder composition, and schema-based PEE resolution without the later native THD execution path. Co-authored-by: Taejin Park <tango4j@gmail.com> Original-Commit: e2d47d7 Original-Commit: 2fb5e1a Original-Commit: d3f4654 Original-Commit: 48e66b3 Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * feat(speechlm2): synchronize encoder chunk microbatches Integrate generalized Parallel Expert Encoder loading and speaker-target routing, add waveform chunk microbatching, and keep FSDP ranks aligned with dummy perception forwards. This remains on the padded/non-THD encoder path. Co-authored-by: Taejin Park <tango4j@gmail.com> Co-authored-by: Kunal Dhawan <kunaldhawan97@gmail.com> Original-Commit: e2d47d7 Original-Commit: 8ef2ff0 Original-Commit: 08f4621 Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * Add Nemotron 3.5 speech prompt support Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * feat(asr): define portable speaker feature contract Keep the padded and streaming phPEE paths self-contained, version the speaker-feature semantics, normalize the embedded Sortformer branch, synchronize rank-dependent routing, align 10 ms diarization outputs, and bound long-form speaker alignment. Co-authored-by: Taejin Park <tango4j@gmail.com> Original-Commit: ad25f14 Original-Commit: 4695c94 Original-Commit: 73c8408 Original-Commit: c656279 Original-Commit: 7b2c3e3 Original-Commit: b6a121f Original-Commit: 8b8ea97 Original-Commit: f86015e Original-Commit: 5159f6c Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * style(speechlm2): format encoder foundation changes Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * fix(asr): bound high-cardinality speaker alignment Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * docs(speechlm2): document Nemotron 3.5 prompt formatter Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * test(asr): cover final speaker alignment contract Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * fix(asr): remove unused compatibility imports Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * fix(asr): preserve streaming device invariants Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * Adding PR-RTTMs compatible code Signed-off-by: Taejin Park <tango4j@gmail.com> * Cleaned up two-branch chucking. removed redundant ggemm code Signed-off-by: Taejin Park <tango4j@gmail.com> * fix(speechlm2): cap speaker targets to the audio grid Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * Addressing all the comments on removal of files and issues Signed-off-by: Taejin Park <tango4j@gmail.com> * test(speechlm2): cover synchronized chunk gradients Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> * docs(asr): escape streaming encoder varargs Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> --------- Signed-off-by: Piotr Żelasko <pzelasko@nvidia.com> Signed-off-by: Taejin Park <tango4j@gmail.com> Co-authored-by: Taejin Park <tango4j@gmail.com>
1 parent 49940a9 commit ea2c046

22 files changed

Lines changed: 2608 additions & 676 deletions

examples/speechlm2/conf/salm_automodel_pee.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ data:
219219
sample_rate: ${data.train_ds.sample_rate}
220220
window_stride: 0.01 # preprocessor hop in seconds
221221
subsampling_factor: 8 # encoder output stride (mel -> target frames)
222-
no_rttm_to_ones: true # cuts without RTTM -> single full-duration speaker
223222

224223
train_ds:
225224
sample_rate: 16000

examples/speechlm2/salm_train.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import os
16+
from datetime import timedelta
1617

1718
import torch
1819
from lightning.pytorch import Trainer, seed_everything
@@ -28,6 +29,25 @@
2829
torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
2930

3031

32+
def _process_group_timeout(cfg):
33+
"""Resolve the NCCL collective timeout from Hydra, then the cluster environment.
34+
35+
The strategy applies ``timeout_minutes`` only to the sub-groups it creates. The
36+
default process group is initialized below, before the Trainer exists, so
37+
without this it keeps the c10d 10-minute default and a slow cold start aborts
38+
the run on a watchdog timeout no config value can raise.
39+
"""
40+
timeout_minutes = OmegaConf.select(cfg, "trainer.strategy.timeout_minutes", default=None)
41+
if timeout_minutes is not None:
42+
return timedelta(minutes=float(timeout_minutes))
43+
44+
timeout_seconds = os.environ.get("TORCH_NCCL_TIMEOUT_S") or os.environ.get("TORCH_NCCL_TIMEOUT_SEC")
45+
if timeout_seconds is not None:
46+
return timedelta(seconds=float(timeout_seconds))
47+
48+
return None
49+
50+
3151
def _create_salm_dataset(tokenizer, data_cfg: DictConfig | dict) -> SALMDataset:
3252
"""Build SALMDataset without forwarding unset options to legacy NeMo packages."""
3353
multispeaker_cfg = data_cfg.get("multispeaker_cfg", None)
@@ -41,7 +61,10 @@ def _create_salm_dataset(tokenizer, data_cfg: DictConfig | dict) -> SALMDataset:
4161
def train(cfg):
4262
OmegaConf.resolve(cfg)
4363
if torch.cuda.is_available():
44-
torch.distributed.init_process_group(backend="nccl")
64+
init_kwargs = {}
65+
if timeout := _process_group_timeout(cfg):
66+
init_kwargs["timeout"] = timeout
67+
torch.distributed.init_process_group(backend="nccl", **init_kwargs)
4568
seed_everything(cfg.data.train_ds.seed)
4669
torch.set_float32_matmul_precision("medium")
4770
trainer = Trainer(**resolve_trainer_cfg(cfg.trainer))

nemo/collections/asr/modules/parallel_expert_encoder.py

Lines changed: 653 additions & 423 deletions
Large diffs are not rendered by default.

nemo/collections/asr/modules/transformer_encoder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,7 @@ class StreamingTransformerEncoder(TransformerEncoder, StreamingEncoder):
931931
that recomputes the mel spectrogram per chunk does: with no look-back the first ~2 mel
932932
frames of every chunk are built from reflect-padded audio instead of the true preceding
933933
samples. Set to ``subsampling_factor`` to give the STFT window its context back.
934-
*args, **kwargs: Forwarded to :class:`TransformerEncoder` (``attn_mode`` is managed
934+
``*args``, ``**kwargs``: Forwarded to :class:`TransformerEncoder` (``attn_mode`` is managed
935935
internally and ignored).
936936
"""
937937

nemo/collections/asr/parts/utils/asr_multispeaker_utils.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import random
1919
from collections import defaultdict
2020
from copy import deepcopy
21-
from typing import Optional, Tuple, Union
21+
from typing import Dict, Optional, Tuple, Union
2222

2323
import numpy as np
2424
import torch.utils.data
@@ -409,7 +409,8 @@ def get_mask_from_segments(
409409
segments: A list of Lhotse Supervision segments iterator.
410410
cut (MonoCut, MixedCut): Lhotse MonoCut or MixedCut instance.
411411
speaker_to_idx_map (dict): A dictionary mapping speaker names to indices.
412-
num_speakers (int): max number of speakers for all cuts ("mask" dim0), 4 by default
412+
num_speakers (int): Optional maximum number of speakers for all cuts ("mask" dim0).
413+
A cut with more speakers is rejected rather than truncated.
413414
feat_per_sec (int): number of frames per second, 100 by default, 0.01s frame rate
414415
415416
Returns:
@@ -542,6 +543,8 @@ def speaker_to_target(
542543
soft_thres: float = 0.5,
543544
return_text: bool = False,
544545
no_rttm_to_ones: bool = True,
546+
preferred_speaker_to_idx_map: Optional[Dict[str, int]] = None,
547+
return_speaker_mapping_status: bool = False,
545548
):
546549
'''
547550
Get rttm samples corresponding to one cut, generate speaker mask numpy.ndarray with shape (num_speaker, hidden_length)
@@ -558,9 +561,15 @@ def speaker_to_target(
558561
return_text (bool): set to True to return the text of the speakers (if it is available), False by default.
559562
no_rttm_to_ones (bool): when a cut has no RTTM/supervisions, synthesize a single full-duration
560563
single-speaker supervision (all-ones target) instead of skipping the cut, True by default.
564+
preferred_speaker_to_idx_map (Optional[Dict[str, int]]): Explicit RTTM speaker-label to target-column
565+
mapping. It is used only when its label set exactly matches the speakers present in the selected RTTM
566+
window. Otherwise, the legacy first-arrival mapping is retained.
567+
return_speaker_mapping_status (bool): If True, also return whether the preferred speaker mapping was used.
561568
562569
Returns:
563-
mask (Tensor): speaker mask with shape (num_speaker, hidden_lenght)
570+
mask (Tensor): speaker mask with shape (num_speaker, hidden_lenght). When
571+
``return_speaker_mapping_status`` is True, the last return value is a bool indicating whether the
572+
preferred mapping was used. This is appended after speaker texts when ``return_text`` is also True.
564573
'''
565574
# get cut-related segments from rttms
566575
if isinstance(a_cut, MixedCut):
@@ -623,20 +632,30 @@ def speaker_to_target(
623632
seen_add = seen.add
624633
speaker_ats = [s.speaker for s in segments_total if not (s.speaker in seen or seen_add(s.speaker))]
625634

626-
speaker_to_idx_map = {spk: idx for idx, spk in enumerate(speaker_ats)}
627-
628635
if num_speakers is None:
629636
num_speakers_dim = len(speaker_ats)
630637
else:
631638
if len(speaker_ats) > num_speakers:
632-
logging.warning(
633-
"Number of speakers in the target %s is greater than "
634-
"the maximum number of speakers %s. Truncating extra speakers. "
635-
"Set the `num_speakers` to higher value to avoid this warning.",
636-
len(speaker_ats),
637-
num_speakers,
639+
raise ValueError(
640+
f"Speaker target contains {len(speaker_ats)} speakers, but num_speakers={num_speakers}. "
641+
"Increase num_speakers instead of dropping speaker supervision."
638642
)
639-
num_speakers_dim = max(len(speaker_ats), num_speakers)
643+
num_speakers_dim = num_speakers
644+
645+
speaker_to_idx_map = {spk: idx for idx, spk in enumerate(speaker_ats)}
646+
used_preferred_speaker_mapping = False
647+
if preferred_speaker_to_idx_map and set(preferred_speaker_to_idx_map) == set(speaker_ats):
648+
preferred_indices = list(preferred_speaker_to_idx_map.values())
649+
if len(set(preferred_indices)) != len(preferred_indices) or any(
650+
not isinstance(idx, int) or idx < 0 or idx >= num_speakers_dim for idx in preferred_indices
651+
):
652+
raise ValueError(
653+
"preferred_speaker_to_idx_map values must be unique integer indices in "
654+
f"[0, {num_speakers_dim}), got {preferred_speaker_to_idx_map}."
655+
)
656+
speaker_to_idx_map = preferred_speaker_to_idx_map
657+
used_preferred_speaker_mapping = True
658+
640659
# initialize mask matrices (num_speaker, encoder_hidden_len)
641660
feat_per_sec = int(a_cut.sampling_rate / num_sample_per_mel_frame) # 100 by default
642661
num_samples = get_hidden_length_from_sample_length(
@@ -655,9 +674,12 @@ def speaker_to_target(
655674
for seg in segments_total:
656675
speaker2text[seg.speaker].append(seg.text)
657676
texts = [' '.join(speaker2text[speaker]) for speaker in speaker_ats]
677+
if return_speaker_mapping_status:
678+
return mask, texts, used_preferred_speaker_mapping
658679
return mask, texts
659-
else:
660-
return mask
680+
if return_speaker_mapping_status:
681+
return mask, used_preferred_speaker_mapping
682+
return mask
661683

662684

663685
def read_seglst(seglst_filepath: str, session_id: Optional[str] = None):

0 commit comments

Comments
 (0)