Files
foxhunt/scripts/monitor_logs.py
jgrusewski 3853988af7 feat(hyperopt): Complete DQN hyperopt analysis and PSO optimizer fix
- Fixed PSO budget calculation bug in ml/src/hyperopt/optimizer.rs
  - Root cause: Division by n_particles in sequential execution
  - Now correctly calculates max_iters = remaining_trials (no division)
  - Result: 50 trials complete instead of 23 (100% vs 46%)

- Added comprehensive DQN hyperopt results analysis
  - 39/50 trials analyzed across 2 RunPod deployments
  - Best hyperparameters identified: LR 4.89e-5 (ultra-low)
  - Created DQN_HYPEROPT_RESULTS_SUMMARY.md with expert validation

- GitLab CI/CD pipeline operational (48 lines fixed)
  - Fixed YAML syntax errors (unquoted colons)
  - All 7 jobs validated and working

- Warning cleanup complete (136 → 0 warnings)
  - Removed 143 lines dead code
  - Fixed visibility, unused imports, Debug traits

- Archived Wave D reports to docs/archive/
  - 8 early stopping reports moved
  - Root directory cleaned up

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 21:49:07 +01:00

559 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
"""
S3 Log Monitoring Script - Real-time training log streaming
Monitors ML training runs on RunPod with S3 log tailing.
Supports both pod-based and run-based monitoring with rich output.
Features:
- List recent pods and runs
- Stream training logs in real-time
- Monitor hyperopt trials.json updates
- Follow mode for continuous streaming
- Configurable timeout
- Color-coded output (errors, warnings, success)
Usage:
# List recent activity
python3 scripts/monitor_logs.py
# Monitor specific run (by run ID)
python3 scripts/monitor_logs.py --run-id run_20251029_145151_hyperopt --follow
# Monitor specific pod (by pod ID)
python3 scripts/monitor_logs.py --pod-id w4srx0tgm5hfgu --follow
# With timeout
python3 scripts/monitor_logs.py --run-id run_20251029_145151_hyperopt --follow --timeout 30m
Requirements:
- .venv activation (source .venv/bin/activate)
- Dependencies: rich, boto3, pydantic-settings
- .env.runpod with S3 credentials
"""
import os
import sys
import re
import time
import argparse
from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta
# Check for .venv activation
is_venv = hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
if not is_venv:
print("=" * 70)
print("ERROR: Not running in a virtual environment (.venv)")
print("=" * 70)
print("This script requires .venv activation to ensure correct dependencies.")
print()
print("To fix this:")
print(" 1. Activate .venv: source .venv/bin/activate")
print(" 2. Install deps: pip install -r runpod/requirements.txt")
print(" 3. Re-run script: python3 scripts/monitor_logs.py --help")
print("=" * 70)
sys.exit(1)
# Get project root for later use
script_dir = Path(__file__).parent
project_root = script_dir.parent
# Add project root to sys.path to ensure local runpod module is used
# This must be done BEFORE importing from runpod to avoid pip package conflict
sys.path.insert(0, str(project_root))
try:
# Import from local runpod module (not pip-installed package)
from runpod import PodMonitor, S3Client, RunPodConfig
from runpod.errors import S3ObjectNotFoundError
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.text import Text
from rich import box
except ImportError as e:
print(f"ERROR: Could not import required modules: {e}")
print()
print("To fix this:")
print(" 1. Ensure .venv is activated: source .venv/bin/activate")
print(" 2. Install dependencies: pip install -r runpod/requirements.txt")
print()
sys.exit(1)
# Change to project root for config loading
os.chdir(project_root)
# Set environment variable to help config find .env.runpod
os.environ.setdefault('RUNPOD_CONFIG_PATH', str(project_root / '.env.runpod'))
console = Console()
def parse_timeout(timeout_str: str) -> Optional[int]:
"""
Parse timeout string to seconds.
Args:
timeout_str: Timeout string (e.g., '30m', '2h', '45s')
Returns:
Timeout in seconds, or None if invalid
"""
match = re.match(r'(\d+)([smh]?)$', timeout_str.lower())
if not match:
return None
value = int(match.group(1))
unit = match.group(2) or 's'
if unit == 'h':
return value * 3600
elif unit == 'm':
return value * 60
else: # 's'
return value
def list_recent_runs(s3_client: S3Client, limit: int = 20) -> list[dict]:
"""
List recent training runs from S3.
Args:
s3_client: S3Client instance
limit: Maximum number of runs to return
Returns:
List of run metadata dicts
"""
try:
# List all training run directories
response = s3_client.s3_client.list_objects_v2(
Bucket=s3_client.bucket,
Prefix="ml_training/training_runs/",
Delimiter="/"
)
runs = []
# Iterate through model types (mamba2, dqn, ppo, tft)
for prefix_obj in response.get('CommonPrefixes', []):
model_prefix = prefix_obj['Prefix']
# List runs for this model type
model_response = s3_client.s3_client.list_objects_v2(
Bucket=s3_client.bucket,
Prefix=model_prefix,
Delimiter="/"
)
for run_prefix_obj in model_response.get('CommonPrefixes', []):
run_path = run_prefix_obj['Prefix']
run_id = run_path.rstrip('/').split('/')[-1]
# Extract model type
model_type = model_prefix.rstrip('/').split('/')[-1]
# Try to get log file metadata for last modified time
log_key = f"{run_path}logs/training.log"
try:
log_obj = s3_client.s3_client.head_object(
Bucket=s3_client.bucket,
Key=log_key
)
last_modified = log_obj['LastModified']
log_size = log_obj['ContentLength']
except:
last_modified = None
log_size = 0
# Check for trials.json (hyperopt indicator)
trials_key = f"{run_path}hyperopt/trials.json"
has_trials = s3_client.object_exists(trials_key)
runs.append({
'run_id': run_id,
'model_type': model_type,
'last_modified': last_modified,
'log_size': log_size,
'has_trials': has_trials,
'log_key': log_key,
'trials_key': trials_key if has_trials else None
})
# Sort by last modified (newest first)
# Use min datetime with UTC timezone for consistency
from datetime import timezone
min_dt = datetime.min.replace(tzinfo=timezone.utc)
runs.sort(key=lambda x: x['last_modified'] or min_dt, reverse=True)
return runs[:limit]
except Exception as e:
console.print(f"[red]Error listing runs: {e}[/red]")
return []
def display_recent_runs(runs: list[dict]) -> None:
"""Display recent runs in a formatted table."""
if not runs:
console.print("[yellow]No recent runs found[/yellow]")
return
table = Table(title="Recent Training Runs", box=box.ROUNDED)
table.add_column("Run ID", style="cyan", no_wrap=True)
table.add_column("Model", style="magenta")
table.add_column("Last Modified", style="green")
table.add_column("Log Size", justify="right", style="blue")
table.add_column("Trials", justify="center")
for run in runs:
run_id = run['run_id']
model_type = run['model_type'].upper()
if run['last_modified']:
# Format as relative time
now = datetime.now(run['last_modified'].tzinfo)
delta = now - run['last_modified']
if delta < timedelta(minutes=1):
time_str = "just now"
elif delta < timedelta(hours=1):
time_str = f"{int(delta.total_seconds() / 60)}m ago"
elif delta < timedelta(days=1):
time_str = f"{int(delta.total_seconds() / 3600)}h ago"
else:
time_str = f"{delta.days}d ago"
else:
time_str = "unknown"
# Format log size
if run['log_size'] > 0:
if run['log_size'] < 1024:
size_str = f"{run['log_size']}B"
elif run['log_size'] < 1024 * 1024:
size_str = f"{run['log_size'] / 1024:.1f}KB"
else:
size_str = f"{run['log_size'] / (1024 * 1024):.2f}MB"
else:
size_str = "-"
trials_str = "" if run['has_trials'] else "-"
table.add_row(run_id, model_type, time_str, size_str, trials_str)
console.print()
console.print(table)
console.print()
console.print("[dim]Use --run-id <RUN_ID> to monitor a specific run[/dim]")
console.print()
def stream_run_logs(
s3_client: S3Client,
run_id: str,
follow: bool = True,
timeout: Optional[int] = None,
poll_interval: int = 5
) -> None:
"""
Stream logs for a specific run.
Args:
s3_client: S3Client instance
run_id: Run ID to monitor
follow: Continue streaming until completion
timeout: Maximum monitoring time in seconds
poll_interval: Polling interval in seconds
"""
# Find the run's log path
try:
# Search for run across all model types
for model_type in ['mamba2', 'dqn', 'ppo', 'tft']:
log_key = f"ml_training/training_runs/{model_type}/{run_id}/logs/training.log"
if s3_client.object_exists(log_key):
trials_key = f"ml_training/training_runs/{model_type}/{run_id}/hyperopt/trials.json"
has_trials = s3_client.object_exists(trials_key)
break
else:
console.print(f"[red]Run not found: {run_id}[/red]")
console.print("[dim]Use 'python3 scripts/monitor_logs.py' to list available runs[/dim]")
return
except Exception as e:
console.print(f"[red]Error finding run: {e}[/red]")
return
# Display header
console.print()
console.print(Panel.fit(
f"[bold cyan]Monitoring Run: {run_id}[/bold cyan]\n"
f"Model: [magenta]{model_type.upper()}[/magenta]\n"
f"Log: [dim]{log_key}[/dim]\n"
f"Hyperopt: [green]Yes[/green]" if has_trials else "[dim]No[/dim]",
title="Log Monitor",
border_style="cyan"
))
console.print()
# Stream logs
log_position = 0
start_time = time.time()
last_trials_check = 0
# Pattern detection
completion_patterns = [
"Training complete",
"Model saved to",
"✓ Training finished",
"SUCCESS:",
"Hyperparameter optimization complete"
]
error_patterns = [
"CUDA out of memory",
"RuntimeError:",
"AssertionError:",
"FAILED:",
"ERROR:",
"panic!"
]
training_complete = False
error_detected = False
try:
console.print("[dim]Streaming logs... (Press Ctrl+C to stop)[/dim]")
console.print("" * 70)
while True:
# Check timeout
if timeout and (time.time() - start_time) > timeout:
console.print()
console.print("[yellow]⏱️ Timeout reached[/yellow]")
break
# Check for new log content
try:
content, log_position = s3_client.tail_log_file(log_key, start_byte=log_position)
if content:
text = content.decode('utf-8', errors='ignore')
lines = text.splitlines()
for line in lines:
# Color-code output based on content
if any(pattern in line for pattern in error_patterns):
console.print(f"[red]{line}[/red]")
error_detected = True
elif "WARN" in line.upper():
console.print(f"[yellow]{line}[/yellow]")
elif "SUCCESS" in line.upper() or "" in line:
console.print(f"[green]{line}[/green]")
elif any(pattern in line for pattern in completion_patterns):
console.print(f"[bold green]{line}[/bold green]")
training_complete = True
else:
console.print(line)
except S3ObjectNotFoundError:
if not follow:
console.print("[yellow]⚠ Log file not found[/yellow]")
return
# Still waiting for log file to appear
pass
# Check trials.json updates (every 30 seconds)
if has_trials and (time.time() - last_trials_check) > 30:
last_trials_check = time.time()
try:
trials_obj = s3_client.s3_client.get_object(
Bucket=s3_client.bucket,
Key=trials_key
)
trials_content = trials_obj['Body'].read().decode('utf-8')
# Parse trial count
import json
trials_data = json.loads(trials_content)
trial_count = len(trials_data)
console.print(f"[dim]📊 Hyperopt trials: {trial_count}[/dim]")
except:
pass
# Check completion
if training_complete or error_detected:
console.print()
console.print("" * 70)
if training_complete:
console.print("[bold green]✅ Training completed![/bold green]")
if error_detected:
console.print("[bold red]❌ Error detected in logs[/bold red]")
break
if not follow:
break
time.sleep(poll_interval)
except KeyboardInterrupt:
console.print()
console.print()
console.print("[yellow]Streaming stopped by user[/yellow]")
def stream_pod_logs(
pod_id: str,
config,
follow: bool = True,
timeout: Optional[int] = None,
poll_interval: int = 5
) -> None:
"""
Stream logs for a specific pod using PodMonitor.
Args:
pod_id: Pod ID to monitor
config: RunPodConfig instance
follow: Continue streaming until completion
timeout: Maximum monitoring time in seconds
poll_interval: Polling interval in seconds
"""
try:
monitor = PodMonitor(pod_id=pod_id, config=config)
# Display pod info
monitor.display_pod_info()
# Stream logs
max_lines = None
if timeout:
# Estimate max lines based on timeout
max_lines = timeout * 10 # Rough estimate
monitor.stream_s3_logs(
follow=follow,
poll_interval=poll_interval,
max_lines=max_lines
)
except Exception as e:
console.print(f"[red]Error monitoring pod: {e}[/red]")
def main():
"""Main execution function."""
parser = argparse.ArgumentParser(
description="Monitor ML training logs on RunPod S3",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# List recent runs
python3 scripts/monitor_logs.py
# Monitor specific run (continuous streaming)
python3 scripts/monitor_logs.py --run-id run_20251029_145151_hyperopt --follow
# Monitor specific pod
python3 scripts/monitor_logs.py --pod-id w4srx0tgm5hfgu --follow
# With timeout (30 minutes)
python3 scripts/monitor_logs.py --run-id run_20251029_145151_hyperopt --follow --timeout 30m
# One-time log snapshot (no streaming)
python3 scripts/monitor_logs.py --run-id run_20251029_145151_hyperopt
Requirements:
- .venv activation (source .venv/bin/activate)
- .env.runpod with S3 credentials
- Dependencies: rich, boto3, pydantic-settings
"""
)
parser.add_argument(
'--run-id',
help='Run ID to monitor (e.g., run_20251029_145151_hyperopt)'
)
parser.add_argument(
'--pod-id',
help='Pod ID to monitor (e.g., w4srx0tgm5hfgu)'
)
parser.add_argument(
'--follow',
action='store_true',
help='Continuously stream logs until completion'
)
parser.add_argument(
'--timeout',
default=None,
help='Maximum monitoring time (e.g., 30m, 2h). Default: no timeout'
)
parser.add_argument(
'--interval',
type=int,
default=5,
help='Log polling interval in seconds (default: 5)'
)
parser.add_argument(
'--limit',
type=int,
default=20,
help='Maximum number of runs to list (default: 20)'
)
args = parser.parse_args()
# Parse timeout
timeout_seconds = None
if args.timeout:
timeout_seconds = parse_timeout(args.timeout)
if timeout_seconds is None:
console.print(f"[red]Invalid timeout format: {args.timeout}[/red]")
console.print("[dim]Use format like '30m', '2h', or '45s'[/dim]")
sys.exit(1)
# Validate arguments
if args.run_id and args.pod_id:
console.print("[red]Error: Cannot specify both --run-id and --pod-id[/red]")
sys.exit(1)
try:
# Load config from explicit path
config = RunPodConfig.load_from_file(project_root / '.env.runpod')
s3_client = S3Client(config)
# Monitor specific run
if args.run_id:
stream_run_logs(
s3_client=s3_client,
run_id=args.run_id,
follow=args.follow,
timeout=timeout_seconds,
poll_interval=args.interval
)
# Monitor specific pod
elif args.pod_id:
stream_pod_logs(
pod_id=args.pod_id,
config=config,
follow=args.follow,
timeout=timeout_seconds,
poll_interval=args.interval
)
# List recent runs
else:
runs = list_recent_runs(s3_client, limit=args.limit)
display_recent_runs(runs)
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
sys.exit(1)
if __name__ == "__main__":
main()