1
Fork 0

feat(log): non-interactive logging, incl. progress bar

This commit is contained in:
Tibo De Peuter 2026-04-08 19:54:05 +02:00
parent faf31567e8
commit 46ae13abc3
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
2 changed files with 38 additions and 34 deletions

View file

@ -1,6 +1,5 @@
import datetime
import random
import sys
import time
from dataclasses import asdict, dataclass
from functools import partial
@ -11,7 +10,6 @@ import jax
import jax.numpy as jnp
import numpy as np
import optax
import tqdm
from flax.training.train_state import TrainState
from experiment_logger import get_logger
@ -367,9 +365,9 @@ class PPOTrainer:
}
self.logger.log(metrics, step=global_step)
def _step(self, env_state, next_obs, next_done, is_tty: bool, iteration: int) -> tuple:
if not is_tty and iteration == 1:
self.logger.info(f">>> [HPC] Starting first rollout (JIT): {time.ctime()}")
def _step(self, env_state, next_obs, next_done, iteration: int) -> tuple:
if iteration == 1:
self.logger.log_non_interactive(f"Starting first rollout (JIT): {time.ctime()}")
(
self.agent_state,
@ -381,20 +379,20 @@ class PPOTrainer:
next_env_state,
) = self._rollout(env_state, next_obs, next_done)
if not is_tty and iteration == 1:
self.logger.info(f">>> [HPC] First rollout completed: {time.ctime()}")
if iteration == 1:
self.logger.log_non_interactive(f"First rollout completed: {time.ctime()}")
storage = self._compute_gae(storage, next_obs, next_done)
if not is_tty and iteration == 1:
self.logger.info(f">>> [HPC] Starting first PPO update (JIT): {time.ctime()}")
if iteration == 1:
self.logger.log_non_interactive(f"Starting first PPO update (JIT): {time.ctime()}")
self.agent_state, loss, pg_loss, v_loss, entropy_loss, approx_kl, self.key = (
self._ppo.update_ppo(self.agent_state, storage, self.key)
)
if not is_tty and iteration == 1:
self.logger.info(f">>> [HPC] First PPO update completed: {time.ctime()}")
if iteration == 1:
self.logger.log_non_interactive(f"First PPO update completed: {time.ctime()}")
avg_episodic_return = float(
jnp.mean(jax.device_get(self.episode_stats.returned_episode_returns)).item()
@ -439,49 +437,41 @@ class PPOTrainer:
"""
self.logger.info(f"running name: {self.run_name}")
is_tty = sys.stdout.isatty()
self.logger.info("[TRAIN]: Resetting environment...")
if not is_tty:
self.logger.info(f">>> [HPC] Initial reset started: {time.ctime()}")
self.logger.log_non_interactive(f"Initial reset started: {time.ctime()}")
env_state = self.env.reset(seed=self.args.seed)
next_obs = _convert_obs_dict_to_array(env_state.observations)
next_done = jnp.zeros(self.args.num_envs, dtype=jnp.bool_)
if not is_tty:
self.logger.info(f">>> [HPC] Initial reset completed: {time.ctime()}")
self.logger.log_non_interactive(f"Initial reset completed: {time.ctime()}")
global_step = 0
start_time = time.time()
iter_bar = tqdm.tqdm(
range(1, self.args.num_iterations + 1),
disable=not is_tty,
)
iter_bar = self.logger.progress_bar(range(1, self.args.num_iterations + 1))
for iteration in iter_bar:
iteration_time_start = time.time()
env_state, next_obs, next_done, loss_info = self._step(
env_state, next_obs, next_done, is_tty=is_tty, iteration=iteration
env_state, next_obs, next_done, iteration=iteration
)
global_step += self.args.num_steps * self.args.num_envs
self._log(global_step, self.episode_stats, start_time, iteration_time_start, loss_info)
if not is_tty:
sps = int(global_step / (time.time() - start_time))
remaining_steps = self.args.total_timesteps - global_step
eta_seconds = int(remaining_steps / sps) if sps > 0 else 0
eta_str = str(datetime.timedelta(seconds=eta_seconds))
sps = int(global_step / (time.time() - start_time))
remaining_steps = self.args.total_timesteps - global_step
eta_seconds = int(remaining_steps / sps) if sps > 0 else 0
eta_str = str(datetime.timedelta(seconds=eta_seconds))
self.logger.info(
f"Iteration {iteration}/{self.args.num_iterations} | "
f"Step {global_step}/{self.args.total_timesteps} | "
f"SPS {sps} | "
f"Return {loss_info.avg_episodic_return:.4f} | "
f"ETA {eta_str}"
)
self.logger.log_non_interactive(
f"Iteration {iteration}/{self.args.num_iterations} | "
f"Step {global_step}/{self.args.total_timesteps} | "
f"SPS {sps} | "
f"Return {loss_info.avg_episodic_return:.4f} | "
f"ETA {eta_str}"
)
if self.args.save_model:
model_path = f"{self.run_dir}/{self.args.exp_name}.cleanrl_model"

View file

@ -10,6 +10,7 @@ import datetime
import json
import logging
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
@ -86,6 +87,7 @@ class UnifiedLogger:
self.use_wandb = use_wandb
self.wandb_available = False
self.wandb_run = None
self.is_interactive = sys.stdout.isatty()
# Setup local storage
self.run_dir = Path(base_dir) / run_name
@ -150,6 +152,18 @@ class UnifiedLogger:
"""Dynamically update the verbosity of the stdout/text logger."""
self._text_logger.setLevel(level)
def log_non_interactive(self, msg: str, *args, **kwargs):
"""Log an info message only if running in a non-interactive environment."""
if not self.is_interactive:
self.info(msg, *args, **kwargs)
def progress_bar(self, iterable=None, *args, **kwargs):
"""Wrapper around tqdm that automatically disables in non-interactive environments."""
import tqdm
kwargs.setdefault("disable", not self.is_interactive)
return tqdm.tqdm(iterable, *args, **kwargs)
def info(self, msg: str, *args, **kwargs):
"""Log an info message to stdout and disk."""
self._text_logger.info(msg, *args, **kwargs)