# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: OpenMDW-1.1 import os from typing import Any, Dict, List, Tuple import pandas as pd import psutil import pynvml import torch import wandb from cosmos_framework.callbacks.every_n import EveryN from cosmos_framework.model._base import ImaginaireModel from cosmos_framework.trainer import ImaginaireTrainer from cosmos_framework.utils import distributed, log from cosmos_framework.utils.easy_io import easy_io def log_prof_data( data_list: List[Dict[str, Any]], iteration: int, ) -> Tuple[pd.DataFrame]: # Create a table to log data with rank information columns = ["iteration", "rank"] + list(data_list[0].keys()) data = [] # Initialize dictionaries to store min and max values for each metric min_values = {key: float("inf") for key in columns[2:]} max_values = {key: float("-inf") for key in columns[2:]} sum_values = {key: 0.0 for key in columns[2:]} count = 0 for _rank, prof_data in enumerate(data_list): row = [iteration, _rank] + [prof_data[key] for key in columns[2:]] data.append(row) count += 1 # Update min, max, and sum values for key in columns[2:]: min_values[key] = min(min_values[key], prof_data[key]) max_values[key] = max(max_values[key], prof_data[key]) sum_values[key] += prof_data[key] # Calculate average values avg_values = {key: sum_values[key] / count for key in columns[2:]} df = pd.DataFrame(data, columns=columns) summary_df = pd.DataFrame({"Avg": avg_values, "Max": max_values, "Min": min_values}) if wandb.run: # Log the table table = wandb.Table(dataframe=df) wandb.log({"DeviceMonitor/prof_data": table}, step=iteration) # Log summary statistics summary = {} for key in columns[2:]: summary[f"DeviceMonitor/min_{key}"] = min_values[key] summary[f"DeviceMonitor/max_{key}"] = max_values[key] summary[f"DeviceMonitor/avg_{key}"] = avg_values[key] wandb.log(summary, step=iteration) return df, summary_df class DeviceMonitor(EveryN): """ A callback to monitor device (CPU/GPU) usage and log it at regular intervals. Args: every_n (int, optional): The frequency at which the callback is invoked. Defaults to 200. step_size (int, optional): The step size for the callback. Defaults to 1. save_s3 (bool, optional): Whether to save the monitoring data to S3. Defaults to False. """ def __init__( self, every_n: int = 200, step_size: int = 1, save_s3: bool = False, upload_every_n_mul: int = 1, log_memory_detail: bool = True, ): super().__init__(every_n=every_n, step_size=step_size) self.name = self.__class__.__name__ self.save_s3 = save_s3 self.s3_save_fp = f"s3://rundir/{self.name}" self.upload_every_n = upload_every_n_mul * every_n self.log_memory_detail = log_memory_detail def on_train_start(self, model, iteration=0): torch.cuda.reset_peak_memory_stats() self.world_size = distributed.get_world_size() self.rank = distributed.get_rank() config_job = self.config.job self.local_dir = f"{config_job.path_local}/{self.name}" if self.rank == 0: os.makedirs(self.local_dir, exist_ok=True) log.info(f"{self.name} callback: local_dir: {self.local_dir}") local_rank = int(os.getenv("LOCAL_RANK", 0)) self.handle = pynvml.nvmlDeviceGetHandleByIndex(local_rank) def every_n_impl( self, trainer: ImaginaireTrainer, model: ImaginaireModel, data_batch: dict[str, torch.Tensor], output_batch: dict[str, torch.Tensor], loss: torch.Tensor, iteration: int, ) -> None: cur_process = psutil.Process(os.getpid()) # cur_process.children(recursive=True) can crash if the dataloader is constantly creating and destroying processes (e.g. calling FFmpeg). try: cpu_memory_usage = sum(p.memory_info().rss for p in [cur_process] + cur_process.children(recursive=True)) except Exception as e: # e.g. psutil.NoSuchProcess log.warning(f"Failed to get CPU memory usage with error {e}") cpu_memory_usage = 0 cpu_mem_gb = cpu_memory_usage / (1024**3) peak_gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3) peak_gpu_mem_reserved_gb = torch.cuda.max_memory_reserved() / (1024**3) temp = torch.cuda.temperature() try: power = torch.cuda.power_draw() except Exception as e: log.warning(f"Failed to get power draw with error {e}") power = 0 util = torch.cuda.utilization() clock = torch.cuda.clock_rate() memory_info = pynvml.nvmlDeviceGetMemoryInfo(self.handle) nvml_used_gpu_mem_gb = memory_info.used / (1024**3) nvml_free_gpu_mem_gb = memory_info.free / (1024**3) prof_data = { "cpu_mem_gb": cpu_mem_gb, "peak_gpu_mem_gb": peak_gpu_mem_gb, "peak_gpu_mem_reserved_gb": peak_gpu_mem_reserved_gb, "nvml_used_gpu_mem_gb": nvml_used_gpu_mem_gb, "nvml_free_gpu_mem_gb": nvml_free_gpu_mem_gb, "temp": temp, "power": power, "util": util, "clock": clock, } data_list = [prof_data] * self.world_size # this is blocking by default if self.world_size > 1: torch.distributed.all_gather_object(data_list, prof_data) torch.distributed.barrier() df, summary_df = log_prof_data(data_list, iteration) if self.save_s3 and self.rank == 0: global_step = iteration // self.step_size should_run = global_step % self.upload_every_n == 0 if should_run: df.to_csv(os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), index=False) summary_df.to_csv(os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), index=True) easy_io.copyfile_from_local( os.path.join(self.local_dir, f"prof_data_{iteration:09d}.csv"), os.path.join(self.s3_save_fp, f"prof_data_{iteration:09d}.csv"), ) easy_io.copyfile_from_local( os.path.join(self.local_dir, f"summary_{iteration:09d}.csv"), os.path.join(self.s3_save_fp, f"summary_{iteration:09d}.csv"), ) if self.rank == 0: log.info(f"{self.name} Stats:\n{summary_df.to_string()}") if self.log_memory_detail: memory_stats = torch.cuda.memory_stats() if wandb.run: wandb_memory_info = {f"mem/{key}": memory_stats[key] for key in memory_stats.keys()} wandb.log(wandb_memory_info, step=iteration) if self.save_s3: global_step = iteration // self.step_size should_run = global_step % self.upload_every_n == 0 if should_run: easy_io.dump( memory_stats, os.path.join(self.s3_save_fp, f"memory_stats_{iteration:09d}.yaml"), ) torch.cuda.reset_peak_memory_stats()