Files
foxhunt/HYPEROPT_LOG_IMPLEMENTATION.md
jgrusewski e61e8f54da feat(ml): Complete hyperopt infrastructure + documentation
Changes:
- CLAUDE.md: Update OOM fix validation status
- Add comprehensive documentation (30+ markdown reports)
- LSTM encoder varmap bug fix (tft/lstm_encoder.rs:290)
- Quantized LSTM layer matching fix (tft/quantized_lstm.rs)
- Hyperopt paths module (ml/src/hyperopt/paths.rs)
- Training path tests for all adapters (DQN, MAMBA-2, PPO, TFT)
- Checkpoint integrity tests
- Script cleanup: Remove 29 obsolete deployment scripts
- Archive old scripts to scripts/archive/
- New deployment utilities: check_gpu_availability.py, monitor_hyperopt.sh

Validation:
- OOM fixes validated: 5/5 trials successful (pod b6kc3mc5lbjiro)
- Batch-size-max 256 tested successfully
- All hyperopt adapters working correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 19:52:21 +01:00

20 KiB

Hyperopt Log File Implementation

Overview

Implementation of proper log file writing for hyperopt training across all ML model adapters.

Requirements

  1. Write training logs to {logs_dir}/training.log
  2. Write hyperopt trial results to {hyperopt_dir}/trials.json
  3. Follow the checkpoint saving pattern (use TrainingPaths.create_all())
  4. Implement for ALL adapters: MAMBA-2, DQN, PPO, TFT

Implementation Pattern

1. Create Log Writer Helper (add to each adapter)

use std::fs::OpenOptions;
use std::io::Write;

/// Write a log entry to the training log file
fn write_training_log(logs_dir: &std::path::Path, message: &str) -> Result<(), std::io::Error> {
    let log_file = logs_dir.join("training.log");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(log_file)?;

    let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
    writeln!(file, "[{}] {}", timestamp, message)?;
    Ok(())
}

/// Write trial results to JSON file
fn write_trial_result<P: serde::Serialize>(
    hyperopt_dir: &std::path::Path,
    trial_result: &crate::hyperopt::traits::TrialResult<P>,
) -> Result<(), std::io::Error> {
    let trials_file = hyperopt_dir.join("trials.json");

    // Read existing trials (if any)
    let mut all_trials = if trials_file.exists() {
        let content = std::fs::read_to_string(&trials_file)?;
        serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_default()
    } else {
        Vec::new()
    };

    // Append new trial
    let trial_json = serde_json::to_value(trial_result)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    all_trials.push(trial_json);

    // Write back to file (pretty printed)
    let content = serde_json::to_string_pretty(&all_trials)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    std::fs::write(&trials_file, content)?;

    Ok(())
}

2. Integration Points in train_with_params()

Add logging at these key points:

fn train_with_params(&mut self, params: Self::Params) -> Result<Self::Metrics, MLError> {
    // 1. Log trial start
    let trial_start = std::time::Instant::now();
    write_training_log(
        &self.training_paths.logs_dir(),
        &format!("=== Starting Trial ===\n{:#?}", params)
    ).ok(); // Don't fail on log write errors

    // ... existing parameter logging ...

    // 2. Create all directories (including logs_dir)
    self.training_paths.create_all()
        .map_err(|e| MLError::ModelError(format!("Failed to create training directories: {}", e)))?;

    // ... training code ...

    // 3. Log training completion
    let duration_secs = trial_start.elapsed().as_secs_f64();
    write_training_log(
        &self.training_paths.logs_dir(),
        &format!("Training completed in {:.2}s: metrics={:#?}", duration_secs, metrics)
    ).ok();

    // 4. Write trial result to JSON
    let trial_result = crate::hyperopt::traits::TrialResult {
        trial_num: 0, // Will be set by optimizer
        params: params.clone(),
        objective: Self::extract_objective(&metrics),
        duration_secs,
    };

    write_trial_result(&self.training_paths.hyperopt_dir(), &trial_result).ok();

    Ok(metrics)
}

Code Changes by File

1. ml/src/hyperopt/adapters/mamba2.rs

Add imports (after line 38):

use std::fs::OpenOptions;
use std::io::Write as IoWrite;

Add helper functions (after line 698, before impl HyperparameterOptimizable):

/// Write a log entry to the training log file
fn write_training_log_mamba2(logs_dir: &std::path::Path, message: &str) -> Result<(), std::io::Error> {
    let log_file = logs_dir.join("training.log");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(log_file)?;

    let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
    writeln!(file, "[{}] {}", timestamp, message)?;
    Ok(())
}

/// Write trial results to JSON file
fn write_trial_result_mamba2(
    hyperopt_dir: &std::path::Path,
    trial_result: &crate::hyperopt::traits::TrialResult<Mamba2Params>,
) -> Result<(), std::io::Error> {
    let trials_file = hyperopt_dir.join("trials.json");

    // Read existing trials (if any)
    let mut all_trials = if trials_file.exists() {
        let content = std::fs::read_to_string(&trials_file)?;
        serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_default()
    } else {
        Vec::new()
    };

    // Append new trial
    let trial_json = serde_json::to_value(trial_result)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    all_trials.push(trial_json);

    // Write back to file (pretty printed)
    let content = serde_json::to_string_pretty(&all_trials)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    std::fs::write(&trials_file, content)?;

    Ok(())
}

Modify train_with_params (add logging at line 705, 799, 889):

fn train_with_params(&mut self, mut params: Self::Params) -> Result<Self::Metrics, MLError> {
    // START: Add trial timing
    let trial_start = std::time::Instant::now();

    // Clamp batch_size to configured bounds (for GPU memory constraints)
    let original_batch_size = params.batch_size;
    // ... existing code ...

    info!("Training MAMBA-2 with 12 hyperparameters:");
    // ... existing parameter logging ...

    // Log trial start
    write_training_log_mamba2(
        &self.training_paths.logs_dir(),
        &format!("=== Starting MAMBA-2 Trial ===\nParams: {:#?}", params)
    ).ok();

    // ... rest of existing code until line 889 ...

    info!("Training completed:");
    info!("  Training loss: {:.6}", metrics.train_loss);
    // ... existing metric logging ...

    // END: Add trial completion logging
    let duration_secs = trial_start.elapsed().as_secs_f64();
    write_training_log_mamba2(
        &self.training_paths.logs_dir(),
        &format!("Training completed in {:.2}s: val_loss={:.6}, train_loss={:.6}, accuracy={:.2}%",
                 duration_secs, metrics.val_loss, metrics.train_loss, metrics.directional_accuracy * 100.0)
    ).ok();

    // Write trial result to JSON
    let trial_result = crate::hyperopt::traits::TrialResult {
        trial_num: 0, // Will be overwritten by optimizer
        params: params.clone(),
        objective: Self::extract_objective(&metrics),
        duration_secs,
    };

    write_trial_result_mamba2(&self.training_paths.hyperopt_dir(), &trial_result).ok();

    Ok(metrics)
}

2. ml/src/hyperopt/adapters/dqn.rs

Add imports (after line 36):

use std::fs::OpenOptions;
use std::io::Write as IoWrite;

Add helper functions (after line 285, before impl HyperparameterOptimizable):

/// Write a log entry to the training log file
fn write_training_log_dqn(logs_dir: &std::path::Path, message: &str) -> Result<(), std::io::Error> {
    let log_file = logs_dir.join("training.log");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(log_file)?;

    let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
    writeln!(file, "[{}] {}", timestamp, message)?;
    Ok(())
}

/// Write trial results to JSON file
fn write_trial_result_dqn(
    hyperopt_dir: &std::path::Path,
    trial_result: &crate::hyperopt::traits::TrialResult<DQNParams>,
) -> Result<(), std::io::Error> {
    let trials_file = hyperopt_dir.join("trials.json");

    // Read existing trials (if any)
    let mut all_trials = if trials_file.exists() {
        let content = std::fs::read_to_string(&trials_file)?;
        serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_default()
    } else {
        Vec::new()
    };

    // Append new trial
    let trial_json = serde_json::to_value(trial_result)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    all_trials.push(trial_json);

    // Write back to file (pretty printed)
    let content = serde_json::to_string_pretty(&all_trials)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    std::fs::write(&trials_file, content)?;

    Ok(())
}

Modify train_with_params (add logging at line 291, 310, 416):

fn train_with_params(&mut self, params: Self::Params) -> Result<Self::Metrics, MLError> {
    // START: Add trial timing
    let trial_start = std::time::Instant::now();

    // Fix 1: Clamp buffer size to max (4GB GPU constraint)
    let clamped_buffer_size = params.buffer_size.min(self.buffer_size_max);

    info!("Training DQN with parameters:");
    // ... existing parameter logging ...

    // Log trial start
    write_training_log_dqn(
        &self.training_paths.logs_dir(),
        &format!("=== Starting DQN Trial ===\nParams: {:#?}", params)
    ).ok();

    // Create all training directories
    self.training_paths.create_all()
        .map_err(|e| MLError::ConfigError {
            reason: format!("Failed to create training directories: {}", e),
        })?;

    info!("Training directories created:");
    info!("  Checkpoints: {:?}", self.training_paths.checkpoints_dir());
    info!("  Logs: {:?}", self.training_paths.logs_dir());  // Add this line
    info!("  Hyperopt: {:?}", self.training_paths.hyperopt_dir());  // Add this line

    // ... rest of existing code until line 416 ...

    info!("Training completed:");
    info!("  Final loss: {:.6}", metrics.train_loss);
    info!("  Avg Q-value: {:.4}", metrics.avg_q_value);

    // END: Add trial completion logging
    let duration_secs = trial_start.elapsed().as_secs_f64();
    write_training_log_dqn(
        &self.training_paths.logs_dir(),
        &format!("Training completed in {:.2}s: loss={:.6}, q_value={:.4}",
                 duration_secs, metrics.train_loss, metrics.avg_q_value)
    ).ok();

    // Write trial result to JSON
    let trial_result = crate::hyperopt::traits::TrialResult {
        trial_num: 0, // Will be overwritten by optimizer
        params: params.clone(),
        objective: Self::extract_objective(&metrics),
        duration_secs,
    };

    write_trial_result_dqn(&self.training_paths.hyperopt_dir(), &trial_result).ok();

    Ok(metrics)
}

3. ml/src/hyperopt/adapters/ppo.rs

Add imports (after line 38):

use std::fs::OpenOptions;
use std::io::Write as IoWrite;

Add helper functions (after line 244, before impl HyperparameterOptimizable):

/// Write a log entry to the training log file
fn write_training_log_ppo(logs_dir: &std::path::Path, message: &str) -> Result<(), std::io::Error> {
    let log_file = logs_dir.join("training.log");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(log_file)?;

    let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
    writeln!(file, "[{}] {}", timestamp, message)?;
    Ok(())
}

/// Write trial results to JSON file
fn write_trial_result_ppo(
    hyperopt_dir: &std::path::Path,
    trial_result: &crate::hyperopt::traits::TrialResult<PPOParams>,
) -> Result<(), std::io::Error> {
    let trials_file = hyperopt_dir.join("trials.json");

    // Read existing trials (if any)
    let mut all_trials = if trials_file.exists() {
        let content = std::fs::read_to_string(&trials_file)?;
        serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_default()
    } else {
        Vec::new()
    };

    // Append new trial
    let trial_json = serde_json::to_value(trial_result)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    all_trials.push(trial_json);

    // Write back to file (pretty printed)
    let content = serde_json::to_string_pretty(&all_trials)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    std::fs::write(&trials_file, content)?;

    Ok(())
}

Modify train_with_params (add logging at line 250, 384):

fn train_with_params(&mut self, params: Self::Params) -> Result<Self::Metrics, MLError> {
    // START: Add trial timing
    let trial_start = std::time::Instant::now();

    info!("Training PPO with parameters:");
    // ... existing parameter logging ...

    // Log trial start
    write_training_log_ppo(
        &self.training_paths.logs_dir(),
        &format!("=== Starting PPO Trial ===\nParams: {:#?}", params)
    ).ok();

    // Create PPO config with trial hyperparameters
    let ppo_config = PPOConfig {
        // ... existing config ...
    };

    // Create directories (add this before creating PPO agent)
    self.training_paths.create_all()
        .map_err(|e| MLError::ModelError(format!("Failed to create training directories: {}", e)))?;

    info!("Training directories created:");
    info!("  Logs: {:?}", self.training_paths.logs_dir());
    info!("  Hyperopt: {:?}", self.training_paths.hyperopt_dir());

    // ... rest of existing code until line 384 ...

    info!("Training completed:");
    info!("  Policy loss: {:.6}", metrics.policy_loss);
    // ... existing metric logging ...

    // END: Add trial completion logging
    let duration_secs = trial_start.elapsed().as_secs_f64();
    write_training_log_ppo(
        &self.training_paths.logs_dir(),
        &format!("Training completed in {:.2}s: val_policy_loss={:.6}, val_value_loss={:.6}",
                 duration_secs, metrics.val_policy_loss, metrics.val_value_loss)
    ).ok();

    // Write trial result to JSON
    let trial_result = crate::hyperopt::traits::TrialResult {
        trial_num: 0, // Will be overwritten by optimizer
        params: params.clone(),
        objective: Self::extract_objective(&metrics),
        duration_secs,
    };

    write_trial_result_ppo(&self.training_paths.hyperopt_dir(), &trial_result).ok();

    Ok(metrics)
}

4. ml/src/hyperopt/adapters/tft.rs

Add imports (after line 38):

use std::fs::OpenOptions;
use std::io::Write as IoWrite;

Add helper functions (after line 275, before impl HyperparameterOptimizable):

/// Write a log entry to the training log file
fn write_training_log_tft(logs_dir: &std::path::Path, message: &str) -> Result<(), std::io::Error> {
    let log_file = logs_dir.join("training.log");
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(log_file)?;

    let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
    writeln!(file, "[{}] {}", timestamp, message)?;
    Ok(())
}

/// Write trial results to JSON file
fn write_trial_result_tft(
    hyperopt_dir: &std::path::Path,
    trial_result: &crate::hyperopt::traits::TrialResult<TFTParams>,
) -> Result<(), std::io::Error> {
    let trials_file = hyperopt_dir.join("trials.json");

    // Read existing trials (if any)
    let mut all_trials = if trials_file.exists() {
        let content = std::fs::read_to_string(&trials_file)?;
        serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_default()
    } else {
        Vec::new()
    };

    // Append new trial
    let trial_json = serde_json::to_value(trial_result)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    all_trials.push(trial_json);

    // Write back to file (pretty printed)
    let content = serde_json::to_string_pretty(&all_trials)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    std::fs::write(&trials_file, content)?;

    Ok(())
}

Modify train_with_params (add logging at line 281, 303, 369):

fn train_with_params(&mut self, params: Self::Params) -> Result<Self::Metrics, MLError> {
    // START: Add trial timing
    let trial_start = std::time::Instant::now();

    info!("Training TFT with parameters:");
    // ... existing parameter logging ...

    // Log trial start
    write_training_log_tft(
        &self.training_paths.logs_dir(),
        &format!("=== Starting TFT Trial ===\nParams: {:#?}", params)
    ).ok();

    // Validate num_heads divides hidden_size
    if params.hidden_size % params.num_heads != 0 {
        // ... existing validation ...
    }

    // Create directories (add this before creating trainer)
    self.training_paths.create_all()
        .map_err(|e| MLError::ModelError(format!("Failed to create training directories: {}", e)))?;

    info!("Training directories created:");
    info!("  Checkpoints: {:?}", self.training_paths.checkpoints_dir());
    info!("  Logs: {:?}", self.training_paths.logs_dir());
    info!("  Hyperopt: {:?}", self.training_paths.hyperopt_dir());

    // ... rest of existing code until line 369 ...

    info!("Training completed:");
    info!("  Training loss: {:.6}", metrics.train_loss);
    // ... existing metric logging ...

    // END: Add trial completion logging
    let duration_secs = trial_start.elapsed().as_secs_f64();
    write_training_log_tft(
        &self.training_paths.logs_dir(),
        &format!("Training completed in {:.2}s: val_loss={:.6}, train_loss={:.6}, rmse={:.4}",
                 duration_secs, metrics.val_loss, metrics.train_loss, metrics.val_rmse)
    ).ok();

    // Write trial result to JSON
    let trial_result = crate::hyperopt::traits::TrialResult {
        trial_num: 0, // Will be overwritten by optimizer
        params: params.clone(),
        objective: Self::extract_objective(&metrics),
        duration_secs,
    };

    write_trial_result_tft(&self.training_paths.hyperopt_dir(), &trial_result).ok();

    Ok(metrics)
}

Verification

After implementation, verify:

  1. Directory Structure:
/runpod-volume/training_runs/{model_name}/run_{run_id}/
├── checkpoints/
│   └── best_model.safetensors
├── logs/
│   └── training.log          # ← NEW
├── hyperopt/
│   └── trials.json           # ← NEW
└── metrics/
  1. Log Format (training.log):
[2025-10-29 14:32:15] === Starting MAMBA-2 Trial ===
Params: Mamba2Params {
    learning_rate: 0.0001,
    batch_size: 32,
    ...
}
[2025-10-29 14:34:23] Training completed in 128.45s: val_loss=0.234567, train_loss=0.198765, accuracy=67.89%
  1. Trial Results (trials.json):
[
  {
    "trial_num": 1,
    "params": {
      "learning_rate": 0.0001,
      "batch_size": 32,
      ...
    },
    "objective": 0.234567,
    "duration_secs": 128.45
  },
  ...
]

Testing

Test with hyperopt examples:

# MAMBA-2
cargo run -p ml --example hyperopt_mamba2_demo --release --features cuda

# DQN
cargo run -p ml --example hyperopt_dqn_demo --release --features cuda

# PPO
cargo run -p ml --example hyperopt_ppo_demo --release --features cuda

# TFT
cargo run -p ml --example hyperopt_tft_demo --release --features cuda

Check outputs:

ls -lh /tmp/ml_training/training_runs/*/run_*/logs/training.log
cat /tmp/ml_training/training_runs/*/run_*/hyperopt/trials.json

Notes

  1. Error Handling: .ok() is used for log writes to avoid failing trials on I/O errors
  2. Timestamps: UTC timestamps for consistency across deployments
  3. JSON Format: Pretty-printed for human readability
  4. Append Mode: Logs append, trials accumulate in JSON array
  5. Trial Numbers: Set to 0 initially, optimizer overwrites with actual trial number

Dependencies

No new dependencies required - uses existing:

  • std::fs::OpenOptions - file I/O
  • std::io::Write - write operations
  • chrono::Utc - timestamps (already imported)
  • serde_json - JSON serialization (already in Cargo.toml)

Status

  • MAMBA-2 adapter (/home/jgrusewski/Work/foxhunt/ml/src/hyperopt/adapters/mamba2.rs)
  • DQN adapter (/home/jgrusewski/Work/foxhunt/ml/src/hyperopt/adapters/dqn.rs)
  • PPO adapter (/home/jgrusewski/Work/foxhunt/ml/src/hyperopt/adapters/ppo.rs)
  • TFT adapter (/home/jgrusewski/Work/foxhunt/ml/src/hyperopt/adapters/tft.rs)
  • Integration testing
  • Runpod deployment verification