Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ce0a6af
Add NemotronH VLM support for video captioning
ronjer30 Oct 2, 2025
8bf8f34
Refactor caption generation and preparation to remove qwen_llm_input …
ronjer30 Oct 7, 2025
5efda69
Updated to use vLLM V1 engine for NemotronH Nano V2 model
ronjer30 Oct 9, 2025
eea9248
Reverted flash-attn back to 2.8.3 to work with latest upstream Nemotr…
ronjer30 Oct 10, 2025
78eb6b5
Enhance NemotronH VLM support for video captioning using HF model ins…
ronjer30 Jan 9, 2026
bea3f95
Fix unit tests and address review comments
ronjer30 Jan 9, 2026
7ecab77
Fix linting errors: remove trailing whitespace and add newline at EOF
ronjer30 Jan 9, 2026
ccca9d7
Improve dtype check in prompt_formatter to handle all floating types
ronjer30 Jan 12, 2026
76c365b
patch
weijiac0619 Feb 27, 2026
1c37443
clean
weijiac0619 Feb 27, 2026
a5d84b7
Merge pull request #1 from ronjer30/fix/cosmos-embed1-transformers-co…
ronjer30 Feb 27, 2026
f60dab8
nemotron preprocessing
weijiac0619 Mar 24, 2026
b8306cd
resolved conflicts
weijiac0619 Mar 24, 2026
d1064a4
stage2 fallback for qwen
weijiac0619 Mar 24, 2026
05bdd95
Merge branch 'main' into feature/nemotron-vlm
weijiac0619 Apr 1, 2026
9ec33e8
clean up
weijiac0619 Apr 1, 2026
d308896
resolved conflicts
weijiac0619 Apr 1, 2026
e13ece3
clean
weijiac0619 Apr 1, 2026
d8ea9ee
Merge branch 'main' into feature/nemotron-vlm
weijiac0619 Apr 1, 2026
c44c94d
comments
weijiac0619 Apr 1, 2026
c9a2c1c
cap transformers <5.0
weijiac0619 Apr 1, 2026
85bcb78
Merge branch 'main' into feature/nemotron-vlm
weijiac0619 Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 193 additions & 1 deletion nemo_curator/models/cosmos_embed1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import importlib.util
from collections.abc import Callable
from pathlib import Path
from typing import Final, Literal, cast

Expand All @@ -38,6 +41,191 @@
}


_HEAD_MASK_VECTOR_DIM: Final[int] = 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need all the changes to the cosmos_embed1.py files ?

@weijiac0619 weijiac0619 Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, they're all needed for trust_remote_code=True to work with newer transformers versions. If the file bulk is a concern, they can be moved to a separate _cosmos_embed1_compat.py.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed with @suiyoubi offline. the patches are not needed with latest cosmos model. will clean this up

_HEAD_MASK_MATRIX_DIM: Final[int] = 2
_HEAD_MASK_TARGET_DIM: Final[int] = 5


def _resolve_symbol(name: str) -> object | None:
for module_name in ("transformers.pytorch_utils", "transformers.modeling_utils"):
if importlib.util.find_spec(module_name) is None:
continue
module = importlib.import_module(module_name)
if hasattr(module, name):
return getattr(module, name)
return None


def _fallback_apply_chunking_to_forward(
forward_fn: Callable[..., object], chunk_size: int, chunk_dim: int, *input_tensors: torch.Tensor
) -> object:
if len(input_tensors) == 0:
msg = "input_tensors cannot be empty"
raise ValueError(msg)

if chunk_size <= 0:
return forward_fn(*input_tensors)

tensor_shape = input_tensors[0].shape[chunk_dim]
if any(t.shape[chunk_dim] != tensor_shape for t in input_tensors):
msg = "All input tensors must have the same shape on chunk_dim"
raise ValueError(msg)
if tensor_shape % chunk_size != 0:
msg = "The dimension to be chunked must be a multiple of chunk_size"
raise ValueError(msg)

num_chunks = tensor_shape // chunk_size
input_chunks = tuple(t.chunk(num_chunks, dim=chunk_dim) for t in input_tensors)
output_chunks = tuple(forward_fn(*chunk_inputs) for chunk_inputs in zip(*input_chunks, strict=False))
return torch.cat(output_chunks, dim=chunk_dim)


def _fallback_find_pruneable_heads_and_indices(
heads: list[int] | set[int], n_heads: int, head_size: int, already_pruned_heads: set[int]
) -> tuple[set[int], torch.Tensor]:
heads_to_prune = set(heads) - already_pruned_heads
mask = torch.ones(n_heads, head_size)
for head in heads_to_prune:
adjusted_head = head - sum(1 for h in already_pruned_heads if h < head)
mask[adjusted_head] = 0
mask = mask.view(-1).contiguous().eq(1)
index = torch.arange(mask.numel())[mask].long()
return heads_to_prune, index


def _fallback_prune_linear_layer(layer: torch.nn.Linear, index: torch.Tensor, dim: int = 0) -> torch.nn.Linear:
index = index.to(layer.weight.device)
weight = layer.weight.index_select(dim, index).clone().detach()
if layer.bias is not None:
bias = layer.bias.clone().detach() if dim == 1 else layer.bias.index_select(0, index).clone().detach()
new_size = list(layer.weight.size())
new_size[dim] = index.size(0)
new_layer = torch.nn.Linear(new_size[1], new_size[0], bias=layer.bias is not None).to(layer.weight.device)
new_layer.weight.requires_grad = False
new_layer.weight.copy_(weight.contiguous())
new_layer.weight.requires_grad = True
if layer.bias is not None:
new_layer.bias.requires_grad = False
new_layer.bias.copy_(bias.contiguous())
new_layer.bias.requires_grad = True
return new_layer


def _ensure_all_tied_weights_keys(model: object) -> None:
if hasattr(model, "all_tied_weights_keys"):
return
tied = getattr(model, "_tied_weights_keys", None)
if tied is None:
model.all_tied_weights_keys = {}
return
if isinstance(tied, dict):
model.all_tied_weights_keys = tied
return
try:
model.all_tied_weights_keys = dict.fromkeys(tied)
except TypeError:
model.all_tied_weights_keys = {}


def _convert_head_mask_to_5d(self: object, head_mask: torch.Tensor, num_hidden_layers: int) -> torch.Tensor:
if head_mask.dim() == _HEAD_MASK_VECTOR_DIM:
head_mask = head_mask.unsqueeze(0).unsqueeze(0).unsqueeze(-1).unsqueeze(-1)
head_mask = head_mask.expand(num_hidden_layers, -1, -1, -1, -1)
elif head_mask.dim() == _HEAD_MASK_MATRIX_DIM:
head_mask = head_mask.unsqueeze(1).unsqueeze(-1).unsqueeze(-1)
if head_mask.dim() != _HEAD_MASK_TARGET_DIM:
msg = f"head_mask.dim != {_HEAD_MASK_TARGET_DIM}, got {head_mask.dim()}"
raise ValueError(msg)
try:
dtype = self.dtype
except Exception: # noqa: BLE001
dtype = next(self.parameters()).dtype
return head_mask.to(dtype=dtype)


def _get_head_mask(
self: object, head_mask: torch.Tensor | None, num_hidden_layers: int, is_attention_chunked: bool = False
) -> torch.Tensor | list[None]:
if head_mask is not None:
head_mask = self._convert_head_mask_to_5d(head_mask, num_hidden_layers)
if is_attention_chunked:
head_mask = head_mask.unsqueeze(-1)
return head_mask
return [None] * num_hidden_layers


def _patch_modeling_utils_symbols(modeling_utils_module: object) -> None:
if not hasattr(modeling_utils_module, "apply_chunking_to_forward"):
symbol = _resolve_symbol("apply_chunking_to_forward") or _fallback_apply_chunking_to_forward
modeling_utils_module.apply_chunking_to_forward = symbol
if not hasattr(modeling_utils_module, "find_pruneable_heads_and_indices"):
symbol = _resolve_symbol("find_pruneable_heads_and_indices") or _fallback_find_pruneable_heads_and_indices
modeling_utils_module.find_pruneable_heads_and_indices = symbol
if not hasattr(modeling_utils_module, "prune_linear_layer"):
symbol = _resolve_symbol("prune_linear_layer") or _fallback_prune_linear_layer
modeling_utils_module.prune_linear_layer = symbol


def _patch_pretrained_model_compat(pre_trained_model_cls: object) -> None:
if not getattr(pre_trained_model_cls, "_nemo_curator_tied_weights_compat_patched", False):
original_mark_tied = getattr(pre_trained_model_cls, "mark_tied_weights_as_initialized", None)
if original_mark_tied is not None:

def _patched_mark_tied_weights_as_initialized(self: object, *args: object, **kwargs: object) -> object:
_ensure_all_tied_weights_keys(self)
return original_mark_tied(self, *args, **kwargs)

pre_trained_model_cls.mark_tied_weights_as_initialized = _patched_mark_tied_weights_as_initialized

original_adjust_tied = getattr(pre_trained_model_cls, "_adjust_tied_keys_with_tied_pointers", None)
if original_adjust_tied is not None:

def _patched_adjust_tied_keys_with_tied_pointers(self: object, *args: object, **kwargs: object) -> object:
_ensure_all_tied_weights_keys(self)
return original_adjust_tied(self, *args, **kwargs)

pre_trained_model_cls._adjust_tied_keys_with_tied_pointers = _patched_adjust_tied_keys_with_tied_pointers
pre_trained_model_cls._nemo_curator_tied_weights_compat_patched = True

if not hasattr(pre_trained_model_cls, "_convert_head_mask_to_5d"):
pre_trained_model_cls._convert_head_mask_to_5d = _convert_head_mask_to_5d
if not hasattr(pre_trained_model_cls, "get_head_mask"):
pre_trained_model_cls.get_head_mask = _get_head_mask


def _patch_transformers_compat() -> None:
"""Patch moved Transformers symbols expected by older Cosmos remote code."""
from transformers import modeling_utils
from transformers.modeling_utils import PreTrainedModel

_patch_modeling_utils_symbols(modeling_utils)
_patch_pretrained_model_compat(PreTrainedModel)


def _patch_cosmos_embed1_modeling_vit(*, weights_dir: str, variant: Literal["224p", "336p", "448p"]) -> None:
"""Patch Cosmos-Embed1 ViT code to avoid meta-tensor .item() failures."""
old_line = "dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule"
new_line = 'dpr = torch.linspace(0, drop_path_rate, depth, device="cpu", dtype=torch.float32).tolist()'
candidates = [
Path(weights_dir) / "modeling_vit.py",
Path.home()
/ ".cache/huggingface/modules/transformers_modules"
/ f"Cosmos_hyphen_Embed1_hyphen_{variant}"
/ "modeling_vit.py",
]

for path in candidates:
try:
if not path.exists():
continue
text = path.read_text(encoding="utf-8")
if old_line not in text:
continue
path.write_text(text.replace(old_line, new_line), encoding="utf-8")
except Exception as e: # noqa: BLE001
logger.warning(f"Could not patch {path}: {e}")


class CosmosEmbed1(ModelInterface):
"""Cosmos-Embed1 embedding model."""

Expand Down Expand Up @@ -78,6 +266,8 @@ def setup(self) -> None:
This method initializes the model and its configuration for processing video and text data.
"""
logger.info("Setting up Cosmos-Embed1 model")
_patch_transformers_compat()
_patch_cosmos_embed1_modeling_vit(weights_dir=self._weights_dir, variant=self.variant)
if not Path(self._weights_dir).exists():
exception = f"Weights directory {self._weights_dir} not found!"
raise FileNotFoundError(exception)
Expand Down Expand Up @@ -201,7 +391,9 @@ def download_weights_on_node(cls, model_dir: str, variant: Literal["224p", "336p
logger.info(f"CosmosEmbed1 {variant} weights downloaded to: {model_dir_path}")

@classmethod
def download_processor_config_on_node(cls, model_dir: str, variant: Literal["224p", "336p", "448p"] = "336p") -> None:
def download_processor_config_on_node(
cls, model_dir: str, variant: Literal["224p", "336p", "448p"] = "336p"
) -> None:
"""Download the processor config for the CosmosEmbed1 model on the node."""
model_dir_path = Path(model_dir) / _COSMOS_EMBED1_VARIANTS_INFO[variant]
model_dir_path.mkdir(parents=True, exist_ok=True)
Expand Down
Loading