# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: OpenMDW-1.1 from __future__ import annotations import torch import torch.distributed as dist import torch.utils.data import wandb from cosmos_framework.model._base import ImaginaireModel from cosmos_framework.utils import distributed from cosmos_framework.utils.callback import Callback class DataStatsCallback(Callback): def __init__( self, logging_iter_multipler: int = 1, save_s3: bool = False, ) -> None: super().__init__() self.logging_iter_multipler = logging_iter_multipler assert self.logging_iter_multipler > 0, "logging_iter_multipler should be greater than 0" self.save_s3 = save_s3 self.wandb_extra_tag = f"@{logging_iter_multipler}" if logging_iter_multipler > 1 else "" self.name = "data_stats" + self.wandb_extra_tag self.data_freq_current = {} self.data_freq_acc = {} self.avg_num_assistant_tokens = [] self.avg_num_real_tokens = [] self.max_num_real_tokens = [] self.min_num_real_tokens = [] # Per-dataset token length tracking self.dataset_token_lengths = {} # dataset_name -> list of avg_num_real_tokens self.dataset_assistant_tokens = {} # dataset_name -> list of avg_num_assistant_tokens self.num_log_current = 0 self.total_count_acc = {} def on_training_step_end( self, model: ImaginaireModel, data_batch: dict[str, torch.Tensor], output_batch: dict[str, torch.Tensor], loss: torch.Tensor, iteration: int = 0, ) -> None: self.num_log_current += 1 dataset_name = data_batch.get("dataset_name", "default") # Handle case where dataset_name gets batched into a list if isinstance(dataset_name, list): assert len(dataset_name) == 1, "dataset_name should be a list of 1" dataset_name = dataset_name[0] if dataset_name in ["default"] and "__url__" in data_batch: # try to get the name from url dataset_name = "/".join(data_batch["__url__"][0].split("/")[:-1]) if dataset_name not in self.data_freq_current: self.data_freq_current[dataset_name] = torch.tensor(0, device="cuda") # [] self.data_freq_current[dataset_name] += 1 if dataset_name not in self.data_freq_acc: self.data_freq_acc[dataset_name] = torch.tensor(0, device="cuda") # [] self.data_freq_acc[dataset_name] += 1 if "avg_num_assistant_tokens" in output_batch: self.avg_num_assistant_tokens.append(output_batch["avg_num_assistant_tokens"]) # Track per-dataset assistant tokens if dataset_name not in self.dataset_assistant_tokens: self.dataset_assistant_tokens[dataset_name] = [] self.dataset_assistant_tokens[dataset_name].append(output_batch["avg_num_assistant_tokens"]) if "avg_num_real_tokens" in output_batch: self.avg_num_real_tokens.append(output_batch["avg_num_real_tokens"]) # Track per-dataset token lengths if dataset_name not in self.dataset_token_lengths: self.dataset_token_lengths[dataset_name] = [] self.dataset_token_lengths[dataset_name].append(output_batch["avg_num_real_tokens"]) if "max_num_real_tokens" in output_batch: self.max_num_real_tokens.append(output_batch["max_num_real_tokens"]) if "min_num_real_tokens" in output_batch: self.min_num_real_tokens.append(output_batch["min_num_real_tokens"]) if iteration % (self.config.trainer.logging_iter * self.logging_iter_multipler) == 0: # Step 1: Gather all dataset names across ranks local_dataset_names = list(self.data_freq_current.keys()) all_dataset_names = [None for _ in range(dist.get_world_size())] dist.all_gather_object(all_dataset_names, local_dataset_names) # Step 2: Create the union of all dataset names union_dataset_names = set() for names in all_dataset_names: union_dataset_names.update(names) union_dataset_names = sorted(list(union_dataset_names)) # Step 3: For any missing dataset name, add dummy _LossRecord with NaN loss for dataset_name in union_dataset_names: if dataset_name not in self.data_freq_acc: self.data_freq_acc[dataset_name] = torch.tensor(0, device="cuda") # [] if dataset_name not in self.data_freq_current: self.data_freq_current[dataset_name] = torch.tensor(0, device="cuda") # [] # Step 4: Calculate the total count of each dataset across all ranks total_count_current = {} for dataset_name in union_dataset_names: acc_tensor = self.data_freq_acc[dataset_name].clone() current_tensor = self.data_freq_current[dataset_name].clone() dist.all_reduce(acc_tensor, op=dist.ReduceOp.SUM) dist.all_reduce(current_tensor, op=dist.ReduceOp.SUM) self.total_count_acc[dataset_name] = acc_tensor.item() total_count_current[dataset_name] = current_tensor.item() if distributed.is_rank0() and wandb.run is not None: info = {} if len(self.avg_num_assistant_tokens) > 0: info["data_stats_tokens/avg_num_assistant_tokens"] = sum(self.avg_num_assistant_tokens) / len( self.avg_num_assistant_tokens ) self.avg_num_assistant_tokens = [] if len(self.avg_num_real_tokens) > 0: info["data_stats_tokens/avg_num_real_tokens"] = sum(self.avg_num_real_tokens) / len( self.avg_num_real_tokens ) self.avg_num_real_tokens = [] if len(self.max_num_real_tokens) > 0: info["data_stats_tokens/max_num_real_tokens"] = max(self.max_num_real_tokens) self.max_num_real_tokens = [] if len(self.min_num_real_tokens) > 0: info["data_stats_tokens/min_num_real_tokens"] = min(self.min_num_real_tokens) self.min_num_real_tokens = [] # Log per-dataset average token lengths for dataset_name in union_dataset_names: if dataset_name in self.dataset_token_lengths and len(self.dataset_token_lengths[dataset_name]) > 0: avg_token_length = sum(self.dataset_token_lengths[dataset_name]) / len( self.dataset_token_lengths[dataset_name] ) info[f"data_stats_avg_tokens_per_dataset/{dataset_name}"] = avg_token_length if ( dataset_name in self.dataset_assistant_tokens and len(self.dataset_assistant_tokens[dataset_name]) > 0 ): avg_assistant_tokens = sum(self.dataset_assistant_tokens[dataset_name]) / len( self.dataset_assistant_tokens[dataset_name] ) info[f"data_stats_avg_assistant_tokens_per_dataset/{dataset_name}"] = avg_assistant_tokens # Reset per-dataset token lengths after logging self.dataset_token_lengths = {} self.dataset_assistant_tokens = {} # Log the valid count per dataset for dataset_name in union_dataset_names: info[f"data_stats_count_acc/{dataset_name}"] = self.total_count_acc[dataset_name] info[f"data_stats_count_current/{dataset_name}"] = total_count_current[dataset_name] self.num_log_current = 0 wandb.log(info, step=iteration) # Create a table of the data stats, columns: Dataset, Accumulated frequency, Current frequency, Accumulated Count, Current Count table_html = "
| Dataset | Accumulated frequency | Current frequency | Accumulated Count | Current Count |
|---|---|---|---|---|
| {name} | {acc_freq} | {current_freq} | {self.total_count_acc[name]} | {total_count_current[name]} |
| Total ({len(union_dataset_names)}) | {acc_freq_sum} | {current_freq_sum} | {total_count_acc_sum} | {total_count_current_sum} |