dwehr's picture
Migrate action viewer to local Cosmos generation
9f818c5
Raw
History Blame Contribute Delete
8.86 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1
import contextlib
import dataclasses
import traceback
from abc import ABC, abstractmethod
from collections.abc import Iterator
from contextlib import contextmanager, nullcontext
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ContextManager, Self, Sequence, final
import torch
import torch.profiler
from cosmos_framework.inference.common.args import GuardrailArgs, SampleArgs, SampleOutputs, SetupArgs
from cosmos_framework.inference.common.init import is_rank0
from cosmos_framework.utils import log
from cosmos_framework.utils.misc import TrainingTimer
if TYPE_CHECKING:
from cosmos_framework.auxiliary.guardrail.common.core import GuardrailRunner
@contextlib.contextmanager
def sync_distributed_errors():
"""Catches local exceptions and synchronizes the error state across all distributed ranks.
Raises a DistributedError on all ranks if ANY rank encountered an exception.
"""
error_flag = torch.zeros(1, dtype=torch.int32, device="cuda") # [1]
local_error: Exception | None = None
try:
yield
except Exception as e:
error_flag += 1
local_error = e
if torch.distributed.is_initialized():
# Sync the error count across all GPUs
torch.distributed.all_reduce(error_flag, op=torch.distributed.ReduceOp.SUM)
if error_flag.item() > 0:
# If we got here, somebody failed.
# Ranks that failed will raise their actual error.
# Ranks that succeeded will raise a generic error so they gracefully abort too.
err_to_raise = local_error if local_error else RuntimeError("A different GPU rank failed.")
raise err_to_raise
@dataclass
class GuardrailRunners:
text: "GuardrailRunner"
video: "GuardrailRunner"
@classmethod
def create(cls, args: GuardrailArgs, /) -> Self:
from cosmos_framework.auxiliary.guardrail.common import presets
return cls(
text=presets.create_text_guardrail_runner(offload_model_to_cpu=args.offload_guardrail_models),
video=presets.create_video_guardrail_runner(offload_model_to_cpu=args.offload_guardrail_models),
)
@dataclass(kw_only=True)
class Inference(ABC):
"""Inference pipeline base class."""
setup_args: SetupArgs
model: torch.nn.Module
guardrails: GuardrailRunners | None
_timer: TrainingTimer | None
_timer_context: list[str] = dataclasses.field(default_factory=list)
@property
@abstractmethod
def model_config(self) -> Any:
"""Get model config."""
@classmethod
@abstractmethod
def _create(cls, setup_args: SetupArgs, /, **kwargs: Any) -> Self:
"""Create instance."""
@abstractmethod
def create_batches(
self, sample_args_list: Sequence[SampleArgs]
) -> Iterator[tuple[list[SampleArgs], dict[str, Any]]]:
"""Create batches of sample data."""
@abstractmethod
def generate_batch(
self, sample_args_list: Sequence[SampleArgs], data_batch: dict[str, Any], *, warmup: bool = False
) -> list[SampleOutputs]:
"""Generate a batch of samples."""
@final
@classmethod
def create(cls, setup_args: SetupArgs, /) -> Self:
"""Create instance."""
timer = TrainingTimer() if setup_args.benchmark else None
guardrails = GuardrailRunners.create(setup_args) if setup_args.guardrails else None
return cls._create(setup_args, guardrails=guardrails, _timer=timer)
@torch.no_grad()
@final
def generate(self, sample_args_list: list[SampleArgs]) -> list[SampleOutputs]:
"""Generate a list of samples."""
# Create batches
try:
with sync_distributed_errors():
batches = self.create_batches(sample_args_list)
except Exception as e:
return [self._handle_sample_exception(sample_args, e) for sample_args in sample_args_list]
# Generate batches
sample_outputs: list[SampleOutputs] = []
for i_batch, (sample_args_batch, data_batch) in enumerate(batches):
log.debug(f"[{i_batch + 1}] Processing batch", rank0_only=False)
if self.setup_args.profile:
profiler = torch.profiler.profile(
activities=[torch.profiler.ProfilerActivity.CPU, torch.profiler.ProfilerActivity.CUDA],
record_shapes=True,
profile_memory=True,
with_stack=True,
)
else:
profiler = contextlib.nullcontext()
with self._get_timer_context("warmup"):
for _ in range(self.setup_args.warmup):
with self._get_timer(f"{self.__class__.__name__}.generate_batch"):
self.generate_batch(sample_args_batch, data_batch, warmup=True)
with self._get_timer(f"{self.__class__.__name__}.generate_batch"), profiler:
sample_outputs.extend(self.generate_batch(sample_args_batch, data_batch))
if self.setup_args.profile and is_rank0():
assert isinstance(profiler, torch.profiler.profile)
sample_args = sample_args_batch[0]
profile_file = sample_args.output_dir / "profile.json.gz"
profiler.export_chrome_trace(str(profile_file))
log.success(f"Saved profile to '{profile_file}'")
return sample_outputs
def _get_timer(self, func_name: str) -> ContextManager:
if self._timer is None:
return nullcontext()
if self._timer_context:
context = ".".join(self._timer_context)
func_name = f"[{context}] {func_name}"
return self._timer(func_name)
@contextmanager
def _get_timer_context(self, func_name: str):
self._timer_context.append(func_name)
try:
yield
finally:
self._timer_context.pop()
def get_timer_results(self) -> dict | None:
if self._timer is None:
return None
return {
"all": self._timer.results,
"average": self._timer.compute_average_results(),
}
def _handle_sample_exception(self, sample_args: SampleArgs, e: Exception) -> SampleOutputs:
msg = f"Error generating sample '{sample_args.name}': {e}"
if not self.setup_args.keep_going:
raise ValueError(msg) from e
log.error(msg)
return SampleOutputs(
args=sample_args.model_dump(mode="json"), status="error", message=msg, stack_trace=traceback.format_exc()
)
@final
def _run_text_guardrail(self, name: str, prompt: str) -> None:
"""Run guardrail checks on the prompt."""
if self.guardrails is None:
return
from cosmos_framework.auxiliary.guardrail.common import presets
if not presets.run_text_guardrail(prompt, self.guardrails.text):
raise ValueError(f"Guardrail blocked prompt '{name}': '{prompt}'")
@final
def _run_video_guardrail(self, name: str, video_cthw: torch.Tensor) -> torch.Tensor:
"""Run guardrail checks on the video and apply face blur."""
if self.guardrails is None:
return video_cthw
processed_video_cthw, message = _run_video_guardrail(self.guardrails.video, video_cthw)
if processed_video_cthw is None:
raise ValueError(f"Guardrail blocked video '{name}': {message}")
return processed_video_cthw
def _run_video_guardrail(
video_guardrail_runner: "GuardrailRunner", video_cthw: torch.Tensor
) -> tuple[torch.Tensor | None, str]:
"""Run video guardrail and apply face blur.
Returns a ``(video_or_none, message)`` tuple. When the guardrail blocks
the video, ``video_or_none`` is ``None`` and ``message`` contains the
underlying reason (unsafe frame ratio, categories, etc.) as produced by
:class:`GuardrailRunner.run_safety_check`.
"""
if video_cthw.ndim != 4:
raise ValueError(f"Video tensor must have 4 dimensions, got {video_cthw.shape}")
frames_thwc = (
(video_cthw * 255.0).clamp(0.0, 255.0).to(torch.uint8).permute(1, 2, 3, 0).detach().cpu().numpy()
) # [T,H,W,C]
# Inline of presets.run_video_guardrail so we can forward `message` (the helper drops it).
is_safe, message = video_guardrail_runner.run_safety_check(frames_thwc)
if not is_safe:
log.critical(f"GUARDRAIL BLOCKED: {message}")
return None, message
frames_thwc = video_guardrail_runner.postprocess(frames_thwc)
video_cthw = (torch.from_numpy(frames_thwc).float().permute(3, 0, 1, 2) / 255.0).to( # [C,T,H,W]
video_cthw.device, dtype=video_cthw.dtype
)
return video_cthw, message