Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

732 lines
30 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.
import copy
import os
import re
import torch
from lightning import LightningModule
from omegaconf import DictConfig
from peft import PeftModel
from torch import Tensor
from torch.distributed.fsdp import fully_shard
from torch.distributed.tensor import Replicate, Shard
from torch.distributed.tensor.parallel import (
ColwiseParallel,
PrepareModuleInput,
RowwiseParallel,
SequenceParallel,
loss_parallel,
parallelize_module,
)
from nemo.collections.common.tokenizers import AutoTokenizer
from nemo.collections.speechlm2.data.utils import get_pad_id
from nemo.collections.speechlm2.parts.hf_hub import HFHubMixin
from nemo.collections.speechlm2.parts.label_prep import maybe_prepend_prompt_tokens, prepare_text_and_asr_labels
from nemo.collections.speechlm2.parts.lora import maybe_install_lora
from nemo.collections.speechlm2.parts.metrics.bleu import BLEU
from nemo.collections.speechlm2.parts.metrics.empty_text import EmptyTextMetric
from nemo.collections.speechlm2.parts.metrics.results_logger import ResultsLogger
from nemo.collections.speechlm2.parts.metrics.turn_taking import TurnTakingMetrics
from nemo.collections.speechlm2.parts.metrics.wer import WER
from nemo.collections.speechlm2.parts.optim_setup import configure_optimizers, is_frozen
from nemo.collections.speechlm2.parts.pretrained import (
load_pretrained_hf,
maybe_load_pretrained_models,
set_model_dict_for_partial_init,
setup_speech_encoder,
)
from nemo.collections.speechlm2.streaming.duplex_stt_inference import DuplexSTTStreamingInference
from nemo.core.neural_types import AudioSignal, LabelsType, LengthsType, NeuralType
from nemo.utils import logging
def maybe_rename_llm_kwargs_for_nemotron(kwargs: dict, model_cfg) -> dict:
"""This is required because Nemotron models have a different signature than other HF models."""
if 'Nemotron' not in model_cfg.pretrained_llm:
return kwargs
cache = kwargs.pop("past_key_values")
if cache is not None:
cache_key = model_cfg.get("cache_key", "past_key_values")
kwargs[cache_key] = cache
return kwargs
class DuplexSTTModel(LightningModule, HFHubMixin):
def __init__(self, cfg: dict) -> None:
assert isinstance(cfg, dict), (
"You must pass the config to DuplexS2SModel as a Python dict to support hyperparameter serialization "
f"in PTL checkpoints (we got: '{type(cfg)=}')."
)
super().__init__()
self.save_hyperparameters()
self.cfg = DictConfig(cfg)
self.source_sample_rate = self.cfg.source_sample_rate
self.validation_save_path = os.path.join(self.cfg.validation_save_path, "validation_logs")
self.predict_user_text = self.cfg.get("predict_user_text", False)
# Load LLM first
llm = load_pretrained_hf(
self.cfg.pretrained_llm,
pretrained_weights=self.cfg.pretrained_weights,
trust_remote_code=self.cfg.get("trust_remote_code", False),
).train()
# Initialize tokenizer with optional special tokens from config
tokenizer_src = self.cfg.get("tokenizer_path", None) or self.cfg.pretrained_llm
self.tokenizer = AutoTokenizer(
tokenizer_src,
use_fast=True,
bos_token=self.cfg.get("bos_token", None),
eos_token=self.cfg.get("eos_token", None),
pad_token=self.cfg.get("pad_token", None),
)
# Extract LLM components with configurable attribute names
llm_attr_name = self.cfg.get("llm_attr_name", "model")
self.llm = getattr(llm, llm_attr_name)
self.lm_head = llm.lm_head
# Extract embedding layer with configurable attribute name
embed_tokens_attr_name = self.cfg.get("embed_tokens_attr_name", "embed_tokens")
self.embed_tokens = getattr(self.llm, embed_tokens_attr_name)
delattr(self.llm, embed_tokens_attr_name)
if self.predict_user_text:
self.asr_head = copy.deepcopy(self.lm_head)
self.embed_asr_tokens = copy.deepcopy(self.embed_tokens)
maybe_install_lora(self)
# Load the pretrained streaming ASR model
setup_speech_encoder(self, pretrained_weights=self.cfg.pretrained_weights)
maybe_load_pretrained_models(self)
self._use_fsdp = False
self._use_tp = False
# Initialize streaming inference engine
self.streaming_inference = DuplexSTTStreamingInference(self)
@property
def text_vocab_size(self):
"""Return the size of the text tokenizer."""
return self.tokenizer.vocab_size
@property
def text_bos_id(self) -> int:
return self.tokenizer.bos_id
@property
def text_eos_id(self) -> int:
return self.tokenizer.eos_id
@property
def text_pad_id(self) -> int:
"""
Text pad ID is used as a 'blank' for frames when the model is not generating text.
DuplexSTTModel Input/Output Format:
- Input: User audio (speech)
- Output: Text tokens only
Text pad ID is used for:
1. Frames during user speech (where the model is listening)
2. Frames after the model completes its text response
Example:
flow: |---user audio---||---assistant text---||-user audio-|
text channel: 0000000000000000 1xxxxxxx00000000002 0000000000000
(model output)
Where:
- 0 indicates PAD ID (model not generating text)
- 1 indicates BOS ID (beginning of assistant response)
- 2 indicates EOS ID (end of assistant response)
- x indicates text tokens corresponding to the assistant's response
"""
return get_pad_id(self.tokenizer)
def forward(
self,
input_embeds: Tensor,
cache=None,
) -> dict[str, Tensor]:
"""
Text prediction only (audio_loss_weight=0).
"""
kwargs = dict(inputs_embeds=input_embeds, past_key_values=cache, use_cache=cache is not None, return_dict=True)
kwargs = maybe_rename_llm_kwargs_for_nemotron(kwargs, self.cfg)
out = self.llm(**kwargs)
B, T = input_embeds.shape[:2]
text_logits = self.lm_head(out['last_hidden_state'])
asr_logits = None
if self.predict_user_text:
asr_in = out['last_hidden_state']
asr_logits = self.asr_head(asr_in) # (B, T, asr_vocab_size)
if not self.training:
if self.cfg.get("inference_pad_boost", None):
text_logits[:, :, self.text_pad_id] += self.cfg.inference_pad_boost
if self.cfg.get("inference_bos_boost", None):
text_logits[:, :, self.text_bos_id] += self.cfg.inference_bos_boost
if self.cfg.get("inference_eos_boost", None):
text_logits[:, :, self.text_eos_id] += self.cfg.inference_eos_boost
ans = {"text_logits": text_logits}
if self.predict_user_text:
ans["asr_logits"] = asr_logits
if cache is not None:
if 'Nemotron' in self.cfg.pretrained_llm:
cache_key = self.cfg.get("cache_key", "cache_params")
ans["cache"] = getattr(out, cache_key, out.get(cache_key))
else:
ans["cache"] = out["past_key_values"]
return ans
def _maybe_zero_out_scale_for_asr(
self, loss_scale: torch.Tensor, text_labels: torch.Tensor, batch: dict
) -> torch.Tensor:
"""
Zero out the loss scale after text_bos_id token for ASR datasets to not penalize the agent being silent in ASR training.
"""
if batch['task'][0] == 'asr':
for i in range(text_labels.shape[0]):
bos_indices = (text_labels[i] == self.text_bos_id).nonzero(as_tuple=True)
if bos_indices[0].numel() > 0:
bos_idx = bos_indices[0][0].item()
loss_scale[i, bos_idx + 1 :, :] = 0
return loss_scale
def prepare_inputs(self, batch: dict):
# Speech encoder forward pass (audio is already augmented in the dataloader)
source_encoded, source_encoded_lens, _ = self.perception(
input_signal=batch["source_audio"],
input_signal_length=batch["source_audio_lens"],
return_encoder_emb=True,
)
source_encoded, source_encoded_lens, target_tokens = maybe_prepend_prompt_tokens(
batch=batch,
embed_fn=self.embed_tokens,
source_encoded=source_encoded,
source_encoded_lens=source_encoded_lens,
text_pad_id=self.text_pad_id,
)
if (diff := target_tokens.shape[1] - source_encoded.shape[1]) < 0:
target_tokens = torch.cat(
[
target_tokens,
(
torch.ones(source_encoded.shape[0], abs(diff), device=source_encoded.device) * self.text_pad_id
).to(torch.long),
],
dim=-1,
)
elif diff > 0:
target_tokens = target_tokens[:, : source_encoded.shape[1]]
inputs = prepare_text_and_asr_labels(
batch=batch,
target_tokens=target_tokens,
source_encoded=source_encoded,
cfg=self.cfg,
text_pad_id=self.text_pad_id,
text_bos_id=self.text_bos_id,
text_eos_id=self.text_eos_id,
use_tp=self._use_tp,
device_mesh=self.device_mesh if self._use_tp else None,
)
source_encoded = inputs["source_encoded"]
text_inputs = inputs["text_inputs"]
text_labels = inputs["text_labels"]
target_token_lens = inputs["target_token_lens"] # Use adjusted lengths from label_prep
asr_inputs = None
if self.predict_user_text:
asr_inputs = inputs["asr_inputs"]
asr_labels = inputs["asr_labels"]
input_embeds = self.embed_tokens(text_inputs) * self.cfg.get("duplex_text_channel_weight", 1.0)
input_embeds.add_(source_encoded[:, :-1] * self.cfg.get("duplex_user_channel_weight", 1.0))
if self.predict_user_text:
asr_inputs_embeds = self.embed_asr_tokens(asr_inputs) * self.cfg.get("duplex_asr_text_weight", 1.0)
input_embeds.add_(asr_inputs_embeds)
seq_mask = torch.ones_like(text_labels.unsqueeze(-1), device=self.device, dtype=torch.bool)
if self.cfg.get("mask_sequence_loss", True):
for i in range(target_token_lens.size(0)):
speech_end_idx = target_token_lens[i]
seq_mask[i, speech_end_idx:, :] = 0
loss_scale = seq_mask.clone().float()
asr_loss_scale = seq_mask.clone().float()
if self.cfg.get("token_loss_weight"):
token_weights = self.cfg.token_loss_weight
pad_weight = token_weights.get("pad", 1.0)
bos_weight = token_weights.get("bos", 1.0)
eos_weight = token_weights.get("eos", 1.0)
text_weight = token_weights.get("text", 1.0)
loss_scale = (
torch.where(
text_labels.unsqueeze(-1) == self.text_pad_id,
pad_weight,
torch.where(
text_labels.unsqueeze(-1) == self.text_bos_id,
bos_weight,
torch.where(text_labels.unsqueeze(-1) == self.text_eos_id, eos_weight, text_weight),
),
)
* seq_mask.float()
)
# Don't penalize the agent replies for ASR training
loss_scale = self._maybe_zero_out_scale_for_asr(loss_scale, text_labels, batch)
if self.predict_user_text:
asr_loss_scale = (
torch.where(
asr_labels.unsqueeze(-1) == self.text_pad_id,
pad_weight,
torch.where(
asr_labels.unsqueeze(-1) == self.text_bos_id,
bos_weight,
torch.where(asr_labels.unsqueeze(-1) == self.text_eos_id, eos_weight, text_weight),
),
)
* seq_mask.float()
)
ans = {
"input_embeds": input_embeds,
"input_lens": source_encoded_lens - 1,
"text_labels": text_labels,
"loss_scale": loss_scale,
"seq_mask": seq_mask,
}
if self.predict_user_text:
ans["asr_labels"] = asr_labels
ans["asr_loss_scale"] = asr_loss_scale
return ans
def training_step(self, batch: dict, batch_idx: int):
for m in (self.perception.preprocessor, self.perception.encoder, self.llm):
if is_frozen(m):
m.eval()
res = {
"learning_rate": torch.as_tensor(
self.trainer.optimizers[0].param_groups[0]['lr'] if self._trainer is not None else 0
)
}
if batch["audio_data"] is not None:
inputs = self.prepare_inputs(batch["audio_data"])
forward_outputs = self(inputs["input_embeds"])
num_frames = inputs["input_lens"].sum()
with loss_parallel():
text_logits = forward_outputs["text_logits"]
asr_logits = None
if self.predict_user_text:
asr_logits = forward_outputs["asr_logits"]
if self.cfg.get("mask_sequence_loss", True):
text_logits = text_logits * inputs["seq_mask"][:, :, 0].unsqueeze(-1)
text_loss = (
torch.nn.functional.cross_entropy(
text_logits.flatten(0, 1),
inputs["text_labels"].flatten(0, 1),
reduction="none",
)
* inputs["loss_scale"][:, :, 0].flatten(0, 1)
).sum(-1) / num_frames
asr_loss = None
if self.predict_user_text:
asr_loss = (
torch.nn.functional.cross_entropy(
asr_logits.flatten(0, 1),
inputs["asr_labels"].flatten(0, 1),
reduction="none",
)
* inputs["asr_loss_scale"][:, :, 0].flatten(0, 1)
).sum(-1) / num_frames
with torch.no_grad():
predicted_tokens = torch.argmax(text_logits, dim=-1) # (B, T)
target_tokens = inputs["text_labels"] # (B, T)
valid_mask = target_tokens != self.text_pad_id
correct_predictions = (predicted_tokens == target_tokens) & valid_mask
if valid_mask.sum() > 0:
token_accuracy = correct_predictions.sum().float() / valid_mask.sum().float()
else:
token_accuracy = torch.tensor(0.0, device=text_logits.device)
loss = self.cfg.text_loss_weight * text_loss
if self.predict_user_text:
loss = loss + self.cfg.get('asr_loss_weight', 1.0) * asr_loss
B, T = inputs["input_embeds"].shape[:2]
ans = {
"audio_loss": loss,
"audio_to_text_loss": text_loss,
"batch": B,
"length": T,
"token_accuracy": token_accuracy,
}
if self.predict_user_text:
ans["asr_loss"] = asr_loss
res.update(ans)
if batch["text_data"] is not None:
text_input_ids = batch["text_data"]["text_tokens"][:, :-1]
text_target = batch["text_data"]["text_tokens"][:, 1:]
text_out = self.llm(
inputs_embeds=self.embed_tokens(text_input_ids),
past_key_values=None,
use_cache=False,
return_dict=True,
)
text_logits = self.lm_head(text_out['last_hidden_state']) # (B, T, Vt)
text_loss = torch.nn.functional.cross_entropy(
text_logits.flatten(0, 1), # (B, T, Vt) -> (*, Vt)
text_target.flatten(0, 1),
ignore_index=self.text_pad_id,
)
res.update(
{
"text_to_text_loss": text_loss,
}
)
res["loss"] = (1.0 - self.cfg.get('text_to_text_loss_weight', 0.0)) * res.get(
"audio_loss", 0.0
) + self.cfg.get('text_to_text_loss_weight', 0.0) * res.get("text_to_text_loss", 0.0)
self.log_dict(res, on_step=True)
return res
def on_validation_epoch_start(self) -> None:
self.results_logger = ResultsLogger(self.validation_save_path).reset()
self.bleu = BLEU().reset()
self.turn_taking_metrics = TurnTakingMetrics(
eos_token_id=self.text_eos_id,
bos_token_id=self.text_bos_id,
tolerance=13,
latency_multiplier=0.08,
).reset()
if self.predict_user_text:
self.src_bleu = BLEU().reset()
self.src_wer = WER().reset()
self.empty_user_text = EmptyTextMetric().reset()
def on_validation_epoch_end(self, prefix="val") -> None:
bleu = self.bleu.compute()
for k, m in bleu.items():
if "qa" not in k and "mmsu" not in k:
self.log(f"{prefix}_{k}", m.to(self.device), on_epoch=True, sync_dist=True)
acc_metrics = self.results_logger.compute_and_save()
for name, result_dict in acc_metrics.items():
if 'acc' in result_dict:
self.log(f"{prefix}_{name}_acc", result_dict['acc'].to(self.device), on_epoch=True, sync_dist=True)
if 'mcq_acc' in result_dict:
self.log(
f"{prefix}_{name}_mcq_acc", result_dict['mcq_acc'].to(self.device), on_epoch=True, sync_dist=True
)
turn_taking_metrics = self.turn_taking_metrics.compute()
for k, m in turn_taking_metrics.items():
self.log(f"{prefix}_{k}", m.to(self.device), on_epoch=True, sync_dist=True)
if self.predict_user_text:
src_bleu = self.src_bleu.compute()
for k, m in src_bleu.items():
self.log(f"{prefix}_src_{k}", m.to(self.device), on_epoch=True, sync_dist=True)
src_wer = self.src_wer.compute()
for k, m in src_wer.items():
self.log(f"{prefix}_src_{k}", m.to(self.device), on_epoch=True, sync_dist=True)
empty_user_text = self.empty_user_text.compute()
for k, m in empty_user_text.items():
self.log(f"{prefix}_src_{k}", m.to(self.device), on_epoch=True, sync_dist=True)
torch.cuda.empty_cache()
torch.cuda.synchronize()
def validation_step(self, batch: dict, batch_idx: int):
for name, dataset_batch in batch.items():
if dataset_batch is None:
continue
dataset_batch = dataset_batch["audio_data"]
prompt_tokens = dataset_batch.get("prompt_tokens", None)
prompt_token_lens = dataset_batch.get("prompt_token_lens", None)
results = self.streaming_inference.offline_inference(
dataset_batch["source_audio"],
dataset_batch["source_audio_lens"],
prompt_tokens=prompt_tokens,
prompt_token_lens=prompt_token_lens,
)
# Strip timestamps for metrics
text_clean = [re.sub(r"<[\|$].*?[\|$]>", "", s).strip() for s in results["text"]]
# Agent text metrics
self.bleu.update(name=name, refs=dataset_batch["target_texts"], hyps=text_clean)
if "source_tokens" in dataset_batch and results["tokens_text"] is not None:
self.turn_taking_metrics.update(
name=name, source_tokens=dataset_batch["source_tokens"], pred_tokens=results["tokens_text"]
)
# User text metrics
if self.predict_user_text:
self.src_bleu.update(name=name, refs=dataset_batch["source_texts"], hyps=results["src_text"])
self.src_wer.update(name=name, refs=dataset_batch["source_texts"], hyps=results["src_text"])
self.empty_user_text.update(name=name, hyps=results["src_text"])
self.results_logger.update(
name=name,
refs=dataset_batch["target_texts"],
hyps=results["text"],
samples_id=dataset_batch['sample_id'],
user_audio=dataset_batch["source_audio"],
user_audio_sr=self.source_sample_rate,
src_refs=dataset_batch["source_texts"],
src_hyps=results["src_text"],
)
def on_test_epoch_start(self) -> None:
return self.on_validation_epoch_start()
def on_test_epoch_end(self) -> None:
return self.on_validation_epoch_end(prefix="test")
def test_step(self, *args, **kwargs):
return self.validation_step(*args, **kwargs)
def predict_step(self, batch: dict, batch_idx: int, dataloader_idx: int = 0):
batch = batch["audio_data"]
prompt_tokens = batch.get("prompt_tokens", None)
prompt_token_lens = batch.get("prompt_token_lens", None)
prediction = self.streaming_inference.offline_inference(
batch["source_audio"],
batch["source_audio_lens"],
input_pad_len=self.cfg.prediction.max_new_seconds * self.cfg.prediction.input_sample_rate,
prompt_tokens=prompt_tokens,
prompt_token_lens=prompt_token_lens,
)
prediction["sample_id"] = batch["sample_id"]
return prediction
def _get_bos_embedding(self) -> torch.Tensor:
"""Get BOS embedding for AR decoding."""
text_bos = torch.full((1,), fill_value=self.text_pad_id, device=self.device)
input_embeds = self.embed_tokens(text_bos)
return input_embeds
def _get_asr_bos_embedding(self) -> torch.Tensor:
"""Get ASR BOS embedding for AR decoding."""
text_bos = torch.full((1,), fill_value=self.text_pad_id, device=self.device)
input_embeds = self.embed_asr_tokens(text_bos)
return input_embeds
def backward(self, *args, **kwargs):
with loss_parallel():
super().backward(*args, **kwargs)
def configure_optimizers(self):
return configure_optimizers(self)
@property
def oomptimizer_schema(self) -> dict:
"""
Return a typing schema for optimal batch size calibration.
"""
return {
"cls": dict,
"inputs": [
{"name": "source_audio", "type": NeuralType(("B", "T"), AudioSignal()), "seq_length": "input"},
{"name": "source_audio_lens", "type": NeuralType(("B",), LengthsType()), "seq_length": "input"},
{
"name": "target_tokens",
"type": NeuralType(("B", "T"), LabelsType()),
"seq_length": "output",
"vocab_size": self.tokenizer.vocab_size,
},
],
}
def configure_model(self) -> None:
device_mesh = self.device_mesh
if device_mesh is None:
return
llm = self.llm
if isinstance(llm, PeftModel):
llm = llm.base_model.model
if (tp_mesh := device_mesh["tensor_parallel"]).size() > 1:
self._use_tp = True
plan = {
"layers.0": PrepareModuleInput(
input_layouts=(Replicate(),),
desired_input_layouts=(Shard(1),),
use_local_output=True,
),
"norm": SequenceParallel(),
}
parallelize_module(llm, tp_mesh, plan)
for transformer_block in llm.layers:
plan = {
"input_layernorm": SequenceParallel(),
"self_attn.q_proj": ColwiseParallel(),
"self_attn.k_proj": ColwiseParallel(),
"self_attn.v_proj": ColwiseParallel(),
"self_attn.o_proj": RowwiseParallel(output_layouts=Shard(1)),
"post_attention_layernorm": SequenceParallel(),
"mlp": PrepareModuleInput(
input_layouts=(Shard(1),),
desired_input_layouts=(Replicate(),),
),
"mlp.gate_proj": ColwiseParallel(),
"mlp.up_proj": ColwiseParallel(),
"mlp.down_proj": RowwiseParallel(output_layouts=Shard(1)),
}
attn_layer = transformer_block.self_attn
try:
config = self.llm.config
num_attention_heads = getattr(config, 'num_attention_heads', None)
num_key_value_heads = getattr(config, 'num_key_value_heads', None)
hidden_size = getattr(config, 'hidden_size', None)
if all([num_attention_heads, num_key_value_heads, hidden_size]):
for attr_name, val in [
("num_attention_heads", num_attention_heads),
("num_key_value_heads", num_key_value_heads),
("hidden_size", hidden_size),
]:
if val % tp_mesh.size() != 0:
logging.warning(
f"config.{attr_name}={val} is not divisible by {tp_mesh.size()=}: "
f"set a different tensor parallelism size to avoid errors."
)
if hasattr(attn_layer, 'num_heads'):
attn_layer.num_heads = num_attention_heads // tp_mesh.size()
elif hasattr(attn_layer, 'num_attention_heads'):
attn_layer.num_attention_heads = num_attention_heads // tp_mesh.size()
if hasattr(attn_layer, 'num_key_value_heads'):
attn_layer.num_key_value_heads = num_key_value_heads // tp_mesh.size()
if hasattr(attn_layer, 'hidden_size'):
attn_layer.hidden_size = hidden_size // tp_mesh.size()
logging.info(
f"Configured tensor parallel for attention: "
f"heads={num_attention_heads // tp_mesh.size()}, "
f"kv_heads={num_key_value_heads // tp_mesh.size()}, "
f"hidden_size={hidden_size // tp_mesh.size()}"
)
else:
raise AttributeError("Required config attributes not found")
except Exception as e:
logging.warning(f"Failed to configure tensor parallel using config: {e}")
logging.warning("Falling back to attention layer attributes...")
try:
for attr in ("num_heads", "num_key_value_heads", "hidden_size"):
if hasattr(attn_layer, attr):
val = getattr(attn_layer, attr)
if val % tp_mesh.size() != 0:
logging.warning(
f"attn_layer.{attr}={val} is not divisible by {tp_mesh.size()=}: "
f"set a different tensor parallelism size to avoid errors."
)
setattr(attn_layer, attr, val // tp_mesh.size())
except Exception as fallback_e:
logging.warning(f"Both config and fallback methods failed: {fallback_e}")
logging.warning("Skipping tensor parallel configuration for this attention layer")
for m in (self.lm_head,):
parallelize_module(
m,
tp_mesh,
ColwiseParallel(
input_layouts=Shard(1),
output_layouts=Shard(-1),
use_local_output=False,
),
)
if (dp_mesh := device_mesh["data_parallel"]).size() > 1:
assert dp_mesh.ndim == 1
self._use_fsdp = True
fsdp_config = {"mesh": dp_mesh}
for idx, layer in enumerate(llm.layers):
llm.layers[idx] = fully_shard(layer, **fsdp_config)
self.embed_tokens = fully_shard(self.embed_tokens, **fsdp_config)
self.llm = fully_shard(self.llm, **fsdp_config)
self.lm_head = fully_shard(self.lm_head, **fsdp_config)
self.perception = fully_shard(self.perception, **fsdp_config)
if self.predict_user_text:
self.asr_head = fully_shard(self.asr_head, **fsdp_config)
self.embed_asr_tokens = fully_shard(self.embed_asr_tokens, **fsdp_config)
def load_state_dict(self, state_dict, strict: bool = True):
try:
return super().load_state_dict(state_dict, strict=strict)
except RuntimeError:
logging.info("Error loading model state_dict !! Retrying with partial initialization!")
model_dict = set_model_dict_for_partial_init(state_dict, self.state_dict())
return super().load_state_dict(model_dict, strict=False)