Contents
Your training script has 47 command-line arguments. You’re managing experiments by creating files like train_v7_lr001_batch32_final_ACTUALLY_FINAL_v2.py. You've got hardcoded hyperparameters scattered across three different Python files. When someone asks "what hyperparameters produced that 94% accuracy model from last week?", you have no idea because you forgot to log them. This is ML experiment hell, and you're living in it.
I spent two years in this chaos before discovering Hydra. It’s a configuration management framework from Facebook Research that transforms experiment organization from nightmare to actually manageable. No more hardcoded values, no more command-line argument spaghetti, no more “what hyperparameters did I use?” mysteries. Just clean, composable, reproducible configuration. Let me show you how to escape ML configuration hell.
What Is Hydra and Why You Need It
Hydra is a framework for managing complex application configurations. For ML, this means organizing hyperparameters, dataset paths, model architectures, and training settings in a way that doesn’t make you want to quit programming.
What Hydra gives you:
- Hierarchical configuration files (YAML)
- Configuration composition and overrides
- Automatic logging of all config
- Multi-run support (hyperparameter sweeps)
- Working directory management
- Type checking for configs
What problem it solves:
- Hardcoded hyperparameters everywhere
- Command-line argument explosion
- Non-reproducible experiments
- Difficult hyperparameter sweeps
- Config sprawl across multiple files
Think of Hydra as the difference between managing configs with argparse and having an actual configuration system. It's infrastructure for experiments that actually scales.
Installation and Basic Setup
Getting started is simple:
bash
pip install hydra-coreThat’s it. Now let’s see the magic.
Before Hydra: The Nightmare
Here’s typical ML code before Hydra:
python
# train.py - THE WRONG WAY
import argparseparser = argparse.ArgumentParser()
parser.add_argument('--lr', type=float, default=0.001)
parser.add_argument('--batch_size', type=int, default=32)
parser.add_argument('--epochs', type=int, default=10)
parser.add_argument('--model', type=str, default='resnet18')
parser.add_argument('--optimizer', type=str, default='adam')
parser.add_argument('--data_path', type=str, default='/data')
parser.add_argument('--output_dir', type=str, default='./outputs')
# ... 40 more arguments
args = parser.parse_args()
# Somewhere else in the code
HIDDEN_DIM = 256 # Hardcoded!
DROPOUT = 0.5 # Also hardcoded!
# Training code that uses args.lr, args.batch_size, etc.
This is terrible. Arguments everywhere, some hardcoded, no structure, impossible to reproduce.
After Hydra: Configuration Sanity
Here’s the same thing with Hydra:
Config File (config.yaml)
yaml
# config/config.yaml
model:
name: resnet18
hidden_dim: 256
dropout: 0.5training:
lr: 0.001
batch_size: 32
epochs: 10
optimizer: adam
data:
path: /data
augmentation: true
output_dir: ./outputs
Training Script
python
# train.py - THE RIGHT WAY
import hydra
from omegaconf import DictConfig, OmegaConf@hydra.main(version_base=None, config_path="config", config_name="config")
def train(cfg: DictConfig):
# Access config values
print(f"Learning rate: {cfg.training.lr}")
print(f"Batch size: {cfg.training.batch_size}")
print(f"Model: {cfg.model.name}")
# Config is automatically logged
print(OmegaConf.to_yaml(cfg))
# Your training code here
model = create_model(cfg.model)
train_model(model, cfg.training)
if __name__ == "__main__":
train()
Run it:
bash
python train.pyOverride values:
bash
python train.py training.lr=0.01 training.batch_size=64That’s it. Clean, organized, reproducible. Every experiment automatically logs its full configuration.
Hierarchical Configuration (Config Groups)
Hydra’s real power comes from composing configurations:
Directory Structure
config/
├── config.yaml
├── model/
│ ├── resnet18.yaml
│ ├── resnet50.yaml
│ └── efficientnet.yaml
├── optimizer/
│ ├── adam.yaml
│ ├── sgd.yaml
│ └── adamw.yaml
└── data/
├── cifar10.yaml
├── imagenet.yaml
└── custom.yamlModel Configs
yaml
# config/model/resnet18.yaml
name: resnet18
hidden_dim: 256
dropout: 0.5
pretrained: trueyaml
# config/model/resnet50.yaml
name: resnet50
hidden_dim: 512
dropout: 0.3
pretrained: trueOptimizer Configs
yaml
# config/optimizer/adam.yaml
name: adam
lr: 0.001
weight_decay: 0.0001
betas: [0.9, 0.999]yaml
# config/optimizer/sgd.yaml
name: sgd
lr: 0.01
momentum: 0.9
weight_decay: 0.0001Main Config (Composition)
yaml
# config/config.yaml
defaults:
- model: resnet18
- optimizer: adam
- data: cifar10training:
batch_size: 32
epochs: 10
output_dir: ./outputs
Now you can swap entire configurations:
bash
# Use ResNet50 instead of ResNet18
python train.py model=resnet50# Use SGD instead of Adam
python train.py optimizer=sgd
# Change both
python train.py model=efficientnet optimizer=adamw
# Override specific values
python train.py model=resnet50 optimizer.lr=0.01 training.batch_size=64
This is game-changing. You’re not managing 50 command-line arguments — you’re composing configurations.

Multi-Run: Hyperparameter Sweeps
Hydra makes hyperparameter sweeps trivial:
bash
# Sweep over learning rates
python train.py --multirun optimizer.lr=0.001,0.01,0.1# Sweep multiple parameters
python train.py --multirun optimizer.lr=0.001,0.01 training.batch_size=32,64,128
# Grid search
python train.py --multirun model=resnet18,resnet50 optimizer=adam,sgd
Hydra runs each combination automatically, organizing outputs in separate directories. No more writing custom sweep scripts.
Structured Configs (Type Safety)
Want type checking for your configs? Use structured configs:
python
# config.py
from dataclasses import dataclass
from hydra.core.config_store import ConfigStore@dataclass
class ModelConfig:
name: str = "resnet18"
hidden_dim: int = 256
dropout: float = 0.5
@dataclass
class TrainingConfig:
lr: float = 0.001
batch_size: int = 32
epochs: int = 10
@dataclass
class Config:
model: ModelConfig = ModelConfig()
training: TrainingConfig = TrainingConfig()
# Register configs
cs = ConfigStore.instance()
cs.store(name="config", node=Config)
python
# train.py
import hydra
from omegaconf import DictConfig
from config import Config@hydra.main(version_base=None, config_name="config")
def train(cfg: Config):
# Type hints work!
lr: float = cfg.training.lr
batch_size: int = cfg.training.batch_size
# Your training code
pass
if __name__ == "__main__":
train()
Now you get:
- IDE autocomplete
- Type checking
- Runtime validation
- Better documentation
IMO, structured configs should be default for any serious project. The type safety prevents stupid bugs.
Working Directory Management
Hydra automatically creates output directories:
python
@hydra.main(version_base=None, config_path="config", config_name="config")
def train(cfg: DictConfig):
# Hydra changes working directory to outputs/YYYY-MM-DD/HH-MM-SS/
# Save model
torch.save(model.state_dict(), 'model.pt') # Saved in experiment dir
# Save logs
with open('training.log', 'w') as f:
f.write(logs)
# Config is automatically saved to .hydra/config.yaml
# Access original working directory
original_cwd = hydra.utils.get_original_cwd()Every experiment gets its own directory with:
- Full configuration (.hydra/config.yaml)
- Overrides (.hydra/overrides.yaml)
- Your outputs (models, logs, etc.)
No more manually creating timestamped directories or losing track of experiment outputs.
Real-World Example: Complete Training Script
Here’s a production-ready example:
Config Structure
yaml
# config/config.yaml
defaults:
- model: resnet18
- optimizer: adam
- _self_data:
path: ./data
num_workers: 4
pin_memory: true
training:
batch_size: 32
epochs: 10
device: cuda
seed: 42
logging:
wandb: true
project: my_project
log_every: 10
checkpoint:
save_every: 5
keep_last: 3
Training Script
python
# train.py
import hydra
from omegaconf import DictConfig, OmegaConf
import torch
import wandb
from pathlib import Path@hydra.main(version_base=None, config_path="config", config_name="config")
def train(cfg: DictConfig):
# Set seed for reproducibility
torch.manual_seed(cfg.training.seed)
# Log config
print(OmegaConf.to_yaml(cfg))
# Initialize wandb
if cfg.logging.wandb:
wandb.init(
project=cfg.logging.project,
config=OmegaConf.to_container(cfg, resolve=True)
)
# Create model
model = create_model(cfg.model)
model = model.to(cfg.training.device)
# Create optimizer
optimizer = create_optimizer(cfg.optimizer, model.parameters())
# Create dataloaders
train_loader, val_loader = create_dataloaders(cfg)
# Training loop
for epoch in range(cfg.training.epochs):
# Train
train_loss = train_epoch(model, train_loader, optimizer, cfg)
# Validate
val_loss = validate(model, val_loader, cfg)
# Log
if cfg.logging.wandb:
wandb.log({
'train_loss': train_loss,
'val_loss': val_loss,
'epoch': epoch
})
# Save checkpoint
if (epoch + 1) % cfg.checkpoint.save_every == 0:
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'train_loss': train_loss,
'val_loss': val_loss
}, f'checkpoint_epoch_{epoch}.pt')
# Save final model
torch.save(model.state_dict(), 'final_model.pt')
print(f"Training complete! Outputs in: {Path.cwd()}")
def create_model(model_cfg):
# Model creation logic using model_cfg
pass
def create_optimizer(opt_cfg, parameters):
# Optimizer creation logic using opt_cfg
pass
def create_dataloaders(cfg):
# Dataloader creation using cfg.data
pass
def train_epoch(model, loader, optimizer, cfg):
# Training logic
pass
def validate(model, loader, cfg):
# Validation logic
pass
if __name__ == "__main__":
train()
Run experiments:
bash
# Default config
python train.py# Change model
python train.py model=resnet50
# Change multiple settings
python train.py model=efficientnet optimizer.lr=0.01 training.batch_size=64
# Hyperparameter sweep
python train.py --multirun optimizer.lr=0.001,0.01,0.1 training.batch_size=32,64
Every experiment automatically gets:
- Unique output directory
- Full configuration saved
- All outputs organized
- Reproducible settings
Integrating with Other Tools
Hydra works great with existing tools:
With Weights & Biases
python
@hydra.main(version_base=None, config_path="config", config_name="config")
def train(cfg: DictConfig):
wandb.init(
project=cfg.project,
config=OmegaConf.to_container(cfg, resolve=True) # Log full config
)
# Training codeWith PyTorch Lightning
python
import pytorch_lightning as pl@hydra.main(version_base=None, config_path="config", config_name="config")
def train(cfg: DictConfig):
model = MyLightningModule(cfg.model)
trainer = pl.Trainer(
max_epochs=cfg.training.epochs,
gpus=cfg.training.gpus
)
trainer.fit(model)
With Optuna (Hyperparameter Optimization)
python
import optuna@hydra.main(version_base=None, config_path="config", config_name="config")
def optimize(cfg: DictConfig):
def objective(trial):
# Suggest hyperparameters
lr = trial.suggest_loguniform('lr', 1e-5, 1e-1)
batch_size = trial.suggest_categorical('batch_size', [16, 32, 64])
# Override config
cfg.optimizer.lr = lr
cfg.training.batch_size = batch_size
# Train and return metric
return train_and_evaluate(cfg)
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
Common Patterns and Best Practices
Pattern 1: Environment-Specific Configs
yaml
# config/config.yaml
defaults:
- model: resnet18
- env: local # or 'cluster', 'cloud'# config/env/local.yaml
data:
path: ./data
num_workers: 2
device: cpu
# config/env/cluster.yaml
data:
path: /shared/data
num_workers: 16
device: cuda
Switch environments easily:
bash
python train.py env=clusterPattern 2: Experiment Configs
yaml
# config/experiment/baseline.yaml
# @package _global_defaults:
- override /model: resnet18
- override /optimizer: adam
training:
lr: 0.001
batch_size: 32
# config/experiment/large_model.yaml
# @package _global_
defaults:
- override /model: resnet50
- override /optimizer: adamw
training:
lr: 0.0001
batch_size: 64
Run experiments by name:
bash
python train.py +experiment=baseline
python train.py +experiment=large_modelPattern 3: Debugging Config
yaml
# config/debug.yaml
defaults:
- configtraining:
epochs: 2
batch_size: 4
data:
num_workers: 0 # Single-threaded for debugging
logging:
log_every: 1
Quick debugging runs:
bash
python train.py --config-name=debugCommon Mistakes to Avoid
Learn from these Hydra failures:
Mistake 1: Modifying Config Objects
python
# Bad - modifies config in-place
cfg.training.lr = 0.01# Good - create new config or use OmegaConf
from omegaconf import OmegaConf
cfg = OmegaConf.merge(cfg, {"training": {"lr": 0.01}})
Configs should be immutable after loading.
Mistake 2: Not Using Config Groups
python
# Bad - one giant config file
# config.yaml with 500 lines# Good - organized groups
# config/model/*.yaml
# config/optimizer/*.yaml
# etc.
Config groups make things composable and maintainable.
Mistake 3: Hardcoding Paths
python
# Bad
data_path = "/home/user/data" # Hardcoded!# Good
data_path = cfg.data.path # From config
Everything should be configurable, especially paths. FYI, hardcoded paths break when you move to different environments.
Mistake 4: Not Logging Configs
python
# Bad - no config tracking
train(cfg)# Good - log config to wandb/mlflow
wandb.init(config=OmegaConf.to_container(cfg))
Always log your full configuration. Future you will thank present you.
The Bottom Line
Hydra transforms ML experiment management from chaos to organized, reproducible science. It’s not just about cleaner code — it’s about being able to reproduce results, sweep hyperparameters efficiently, and actually know what settings produced what results.
Use Hydra when:
- Running multiple experiments
- Need reproducibility
- Hyperparameter sweeps are common
- Working in a team
- Configs are getting complex
Skip Hydra when:
- One-off scripts
- Configs are trivial
- Learning ML basics (focus on fundamentals first)
For serious ML work, Hydra should be in your stack from day one. The small upfront investment pays off massively when you’re running your 50th experiment and can actually reproduce results from experiment #3.
Installation:
bash
pip install hydra-coreStop managing configs with command-line arguments and hardcoded values. Start using Hydra. Your experiments will be cleaner, more reproducible, and actually manageable. The difference between “I think these were the hyperparameters” and “here’s the exact config that produced that result” is the difference between amateur hour and professional ML engineering. :)