mirror of
https://github.com/hanzoai/gym-v1.git
synced 2026-11-02 15:10:54 +00:00
252 lines
7.5 KiB
Python
252 lines
7.5 KiB
Python
import torch
|
|
from typing import Any, Dict, List
|
|
from collections import OrderedDict
|
|
|
|
|
|
class LogModule:
|
|
def __config__(self, remove_keys: List[str] = None):
|
|
config = extract_config(self)
|
|
|
|
if remove_keys:
|
|
for key in remove_keys:
|
|
if key in config:
|
|
del config[key]
|
|
|
|
return config
|
|
|
|
|
|
def extract_config(obj, max_depth=10, current_depth=0):
|
|
"""
|
|
Extract serializable configuration from an object for wandb logging.
|
|
|
|
This function safely extracts attributes from objects while avoiding
|
|
unpickleable items like tensors, functions, modules, etc.
|
|
|
|
Args:
|
|
obj: The object to extract config from
|
|
max_depth: Maximum recursion depth to avoid infinite loops
|
|
current_depth: Current recursion depth (internal use)
|
|
|
|
Returns:
|
|
dict: A dictionary containing only serializable attributes
|
|
"""
|
|
if current_depth >= max_depth:
|
|
return str(type(obj).__name__)
|
|
|
|
if obj is None:
|
|
return None
|
|
|
|
# Handle primitive types
|
|
if isinstance(obj, (int, float, str, bool)):
|
|
return obj
|
|
|
|
# Handle sequences (but avoid strings which are also sequences)
|
|
if isinstance(obj, (list, tuple)) and not isinstance(obj, str):
|
|
return [
|
|
extract_config(item, max_depth, current_depth + 1) for item in obj[:10]
|
|
] # Limit to first 10 items
|
|
|
|
# Handle dictionaries
|
|
if isinstance(obj, dict):
|
|
result = {}
|
|
for key, value in obj.items():
|
|
if isinstance(key, str) and len(result) < 50: # Limit number of keys
|
|
result[key] = extract_config(value, max_depth, current_depth + 1)
|
|
return result
|
|
|
|
if isinstance(obj, torch.device):
|
|
return obj.__str__()
|
|
|
|
# Skip unpickleable types
|
|
if isinstance(
|
|
obj,
|
|
(
|
|
torch.Tensor,
|
|
torch.nn.Module,
|
|
torch.optim.Optimizer,
|
|
torch.nn.Parameter,
|
|
torch.dtype,
|
|
),
|
|
):
|
|
if isinstance(obj, torch.Tensor):
|
|
return f"<Tensor {list(obj.shape)}>"
|
|
elif isinstance(obj, torch.nn.Module):
|
|
return f"<Module {type(obj).__name__}>"
|
|
elif isinstance(obj, torch.optim.Optimizer):
|
|
return f"<Optimizer {type(obj).__name__}>"
|
|
else:
|
|
return f"<{type(obj).__name__}>"
|
|
|
|
# Skip functions, methods, and other callables
|
|
if callable(obj):
|
|
return f"<function {getattr(obj, '__name__', 'unknown')}>"
|
|
|
|
# Handle objects with __dict__ (like config objects)
|
|
if hasattr(obj, "__dict__"):
|
|
result = {}
|
|
for key, value in obj.__dict__.items():
|
|
if (
|
|
not key.startswith("_") and len(result) < 50
|
|
): # Skip private attributes
|
|
result[key] = extract_config(
|
|
value, max_depth, current_depth + 1
|
|
)
|
|
return result
|
|
|
|
# For other objects, try to get basic info
|
|
if type(obj) in [float, int, str, bool]:
|
|
return obj
|
|
else:
|
|
return f"<{type(obj).__name__}>"
|
|
|
|
|
|
def create_config(
|
|
model: torch.nn.Module, strategy, train_node, extra_config: Dict[str, Any] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a comprehensive configuration from model, strategy, and train_node objects.
|
|
|
|
Args:
|
|
model: The PyTorch model to extract configuration from
|
|
strategy: Training strategy object with __config__ method
|
|
train_node: Training node object with __config__ method
|
|
extra_config: Additional configuration to include (optional)
|
|
|
|
Returns:
|
|
dict: A complete configuration dictionary suitable for any logger
|
|
"""
|
|
config = {}
|
|
|
|
# Add strategy and train_node configurations if they exist
|
|
if strategy and hasattr(strategy, '__config__'):
|
|
config["strategy"] = strategy.__config__()
|
|
if train_node and hasattr(train_node, '__config__'):
|
|
config.update(train_node.__config__())
|
|
|
|
# Model information
|
|
if model:
|
|
config.update(
|
|
{
|
|
"model_name": model.__class__.__name__,
|
|
# "model_config": extract_config(model),
|
|
}
|
|
)
|
|
|
|
# Try to get parameter count
|
|
if hasattr(model, "get_num_params"):
|
|
config["model_parameters"] = model.get_num_params() / 1e6
|
|
else:
|
|
# Fallback to counting parameters
|
|
config["model_parameters"] = (
|
|
sum(p.numel() for p in model.parameters()) / 1e6
|
|
)
|
|
|
|
# Extra configuration
|
|
if extra_config:
|
|
for key, value in extra_config.items():
|
|
config[key] = extract_config(value)
|
|
|
|
return config
|
|
|
|
|
|
def log_model_summary(model: torch.nn.Module) -> Dict[str, Any]:
|
|
"""
|
|
Create a summary of model architecture suitable for logging.
|
|
|
|
Args:
|
|
model: The PyTorch model
|
|
|
|
Returns:
|
|
dict: Model summary information
|
|
"""
|
|
summary = {
|
|
"model_class": model.__class__.__name__,
|
|
"model_module": model.__class__.__module__,
|
|
}
|
|
|
|
try:
|
|
# Parameter count
|
|
if hasattr(model, "get_num_params"):
|
|
summary["total_params"] = model.get_num_params()
|
|
else:
|
|
summary["total_params"] = sum(p.numel() for p in model.parameters())
|
|
|
|
summary["total_params_M"] = summary["total_params"] / 1e6
|
|
|
|
# Trainable parameters
|
|
summary["trainable_params"] = sum(
|
|
p.numel() for p in model.parameters() if p.requires_grad
|
|
)
|
|
summary["trainable_params_M"] = summary["trainable_params"] / 1e6
|
|
|
|
# Model config if available
|
|
if hasattr(model, "config"):
|
|
summary["config"] = extract_config(model.config)
|
|
|
|
# Layer information
|
|
layer_types = {}
|
|
for name, module in model.named_modules():
|
|
module_type = type(module).__name__
|
|
if module_type != model.__class__.__name__: # Skip the root module
|
|
layer_types[module_type] = layer_types.get(module_type, 0) + 1
|
|
summary["layer_types"] = layer_types
|
|
|
|
except Exception as e:
|
|
summary["error"] = f"Error extracting model summary: {str(e)}"
|
|
|
|
return summary
|
|
|
|
|
|
def safe_log_dict(data: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
"""
|
|
Convert a dictionary to a wandb-safe format.
|
|
|
|
Args:
|
|
data: Dictionary to convert
|
|
prefix: Prefix to add to keys
|
|
|
|
Returns:
|
|
dict: Wandb-safe dictionary
|
|
"""
|
|
safe_dict = {}
|
|
|
|
for key, value in data.items():
|
|
safe_key = f"{prefix}_{key}" if prefix else key
|
|
safe_dict[safe_key] = extract_config(value)
|
|
|
|
return safe_dict
|
|
|
|
def print_dataset_size(dataset: torch.utils.data.Dataset):
|
|
import pickle
|
|
import io
|
|
|
|
buffer = io.BytesIO()
|
|
pickle.dump(dataset, buffer, protocol=pickle.HIGHEST_PROTOCOL)
|
|
print(f"Dataset size: {buffer.tell() // 1024 // 1024} MB")
|
|
|
|
def _average_model_states(model_states: Dict[int, OrderedDict]) -> OrderedDict:
|
|
"""
|
|
Average model state dictionaries from multiple processes.
|
|
"""
|
|
if not model_states:
|
|
return None
|
|
|
|
averaged_state = OrderedDict()
|
|
first_state = list(model_states.values())[0]
|
|
|
|
for param_name in first_state.keys():
|
|
if first_state[param_name].dtype != torch.int64:
|
|
param_stack = torch.stack(
|
|
[state[param_name] for state in model_states.values()]
|
|
)
|
|
averaged_state[param_name] = torch.mean(param_stack, dim=0)
|
|
|
|
return averaged_state
|
|
|
|
def get_device():
|
|
if torch.cuda.is_available():
|
|
return "cuda"
|
|
elif torch.backends.mps.is_available():
|
|
return "mps"
|
|
else:
|
|
return "cpu" |