Skip to content
SR-Forge

Configuration System

This page covers the configuration system — how to define your entire experiment in YAML so your experiments are fully reproducible and easy to modify.

SR-Forge uses a YAML-based configuration system that lets you define your entire experiment — model, optimizer, loss, dataset, transforms, runners, trainer, and hooks — in a single file. The ConfigResolver reads that YAML and recursively instantiates every Python object it describes, wiring them together automatically.

For how this config is used inside training scripts, see Writing Scripts.

The result: you can change your model, loss function, or preprocessing pipeline without touching a single line of Python.


Quick Reference

Config syntax — the two magic keys:

_target: my_project.models.MyModel       # What class to create
params:                                   # What arguments to pass
  hidden_dim: 128

Equivalent Python:

from my_project.models import MyModel
model = MyModel(hidden_dim=128)

Cheat sheet:

Feature Syntax Purpose
Instantiate _target + params Create any Python object
Full path _target: my_project.models.MyModel Use full module path
IO binding io: key Bind parameters to Entry fields
Reference ${ref:path.to.object} Reuse an already-instantiated object
Interpolation ${path.to.value} Copy a raw config value
Nesting _target inside params Recursive instantiation

Don't confuse ${path} with ${ref:path}

These look similar but behave very differently:

  • ${loss} copies the raw YAML config — each use creates a separate Python object.
  • ${ref:loss} returns the same Python object (shared instance).

Use ${} to reuse config templates. Use ${ref:} to share a live object.


The Problem: Why Config-Driven?

Consider a typical training setup:

# Without config — everything hardcoded
model = MyModel(hidden_dim=128, num_layers=4, dropout=0.1)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[10, 20])
loss = LossCombiner(losses=[L1(weight=1.0), SSIM(weight=0.01)])
dataset = LazyDataset(root="/data/experiment/train", depth=0)
transforms = [Divide(255.0).set_io({"inputs": {"image": "lr"}}), ToFloat().set_io({"inputs": {"image": "lr"}})]

Every time you want to try a different model, loss, or dataset, you have to edit Python code. That means:

  • You risk introducing bugs in training scripts
  • You can't easily compare experiments (which parameters changed?)
  • You can't reproduce a colleague's run without their exact code

The SR-Forge config system solves this:

# Everything in one YAML file — change it, run it, track it
model:
  _target: MyModel
  params:
    hidden_dim: 128
    num_layers: 4
    dropout: 0.1

optimizer:
  _target: torch.optim.Adam
  params:
    lr: 0.001

Golden Rule

The YAML config is the single source of truth for your experiment. If it's not in the config, it didn't happen.


How It Works: The _target / params Pattern

Every object in SR-Forge is described by two keys:

_target: module.path.ClassName   # Which class to instantiate
params:                           # Constructor arguments (kwargs)
  arg1: value1
  arg2: value2

The ConfigResolver reads this and calls:

from module.path import ClassName
instance = ClassName(arg1=value1, arg2=value2)

Using Full Module Paths

You can specify any importable Python class:

# Your project classes
model:
  _target: my_project.models.MyModel
  params:
    hidden_dim: 128

# PyTorch classes work too
optimizer:
  _target: torch.optim.AdamW
  params:
    lr: 0.0005
    weight_decay: 0

# Scheduler
lr_scheduler:
  _target: torch.optim.lr_scheduler.ReduceLROnPlateau
  params:
    mode: "min"
    factor: 0.5
    patience: 10

When ConfigResolver encounters a _target value, it splits the string to derive the module path and class name — for example, my_project.models.MyModel becomes module my_project.models and class MyModel. The class is then imported and instantiated automatically.

Recursive Instantiation

The real power of ConfigResolver is recursion. When a parameter value itself contains _target, it gets instantiated first:

loss:
  _target: srforge.loss.schedule.LossScheduler
  params:
    schedule:
      0:                                      # At epoch 0, use this loss:
        _target: srforge.loss.combiner.LossCombiner
        params:
          losses:
            - _target: srforge.loss.metrics.L1
              params:
                weight: 1.0
            - _target: srforge.loss.metrics.SSIM
              params:
                weight: 0.01
      50:                                     # At epoch 50, switch to this:
        _target: srforge.loss.combiner.LossCombiner
        params:
          losses:
            - _target: srforge.loss.metrics.L1
              params:
                weight: 1.0
            - _target: srforge.loss.metrics.SSIM
              params:
                weight: 0.05

The resolver resolves this bottom-up:

  1. Creates L1(weight=1.0) and SSIM(weight=0.01)
  2. Creates LossCombiner(losses=[L1_instance, SSIM_instance]) for epoch 0
  3. Creates another LossCombiner for epoch 50
  4. Creates LossScheduler(schedule={0: combiner_0, 50: combiner_50})

You can nest as deeply as you need — each _target + params block is resolved recursively.

Params: None, Empty, and Missing

# No parameters needed — use empty dict
model:
  _target: Identity
  params: {}

# Also valid: params can be null
model:
  _target: Identity
  params:

Both produce the same result: Identity() with no arguments.


Config Interpolation: ${path}

YAML configs support OmegaConf interpolation — referencing other config values using ${path}:

loss:
  _target: srforge.loss.combiner.LossCombiner
  params:
    losses:
      - _target: srforge.loss.metrics.L1
        params:
          weight: 1.0

# Reuse the exact same config structure for validation metrics
validation_metrics: ${loss}

This copies the raw config value — both loss and validation_metrics produce the same object structure when instantiated. Common patterns:

preprocessing:
  training:
    - _target: srforge.transform.data.Multiply
      params:
        value: 2.0
      io:
        inputs: [{image: x}, {image: y}]

  # Reuse training preprocessing for validation
  validation: ${preprocessing.training}

postprocessing:
  training: []
  # Same postprocessing for validation
  validation: ${postprocessing.training}

Key point: ${path} resolves at config-load time (before any objects are created). It copies the raw YAML structure, not instantiated objects.

Separate instances

Because ${path} copies config, each copy is instantiated independently. validation_metrics: ${loss} creates a second loss object — it does not reuse the training loss. If you need the same instance, use ${ref:loss} instead.


Object References: ${ref:path}

Sometimes you need to reference an already-instantiated object, not just copy config. The ${ref:path} syntax does this:

shared_backbone:
  _target: srforge.models.SomeBackbone
  params:
    features: 64

model:
  _target: srforge.models.SomeModel
  params:
    backbone: ${ref:shared_backbone}    # Same Python object, not a copy

When the resolver encounters ${ref:shared_backbone}:

  1. It checks if shared_backbone was already instantiated
  2. If yes, it returns the same Python object (same memory reference)
  3. If no, it instantiates it first, caches it, then returns it

This ensures object identity — both references point to the exact same instance:

resolved = resolver.resolve_all()
assert resolved["model"].backbone is resolved["shared_backbone"]  # True — same object

Method Chains

You can call no-arg methods on referenced objects:

optimizer:
  _target: torch.optim.AdamW
  params:
    params: ${ref:model}.trainable_params()   # resolves model, then calls method
    lr: 0.0005

Chained calls work too: ${ref:obj}.foo().bar().

Reference vs Interpolation

Feature ${path} ${ref:path}
Resolves At config load time At instantiation time
Returns Raw config value (YAML) Instantiated Python object
Identity Creates separate objects Shares the same object
Use when Reusing config structure Sharing runtime objects

Nested References

References can point to nested paths using dot notation:

group:
  inner:
    _target: srforge.data.Entry
    params:
      name: "inner"

alias: ${ref:group.inner}        # Resolves to the Entry instance

Circular Reference Detection

The resolver detects circular references and raises a clear error:

# This will raise ValueError: "Circular reference detected"
a: ${ref:b}
b: ${ref:a}

Legacy syntax

The older "%{path}" form still works (requires YAML quoting) but ${ref:path} is preferred because it needs no quoting.

Which Should I Use?

You want to… Use Why
Reuse a config block (e.g., same transforms for train and val) ${path} Each dataset gets its own independent pipeline
Share one object (e.g., optimizer needs the model's params) ${ref:path} Must be the exact same Python object
Call a method on another object ${ref:path}.method() Only references support method chains
Copy a scalar value (number, string) ${path} ${ref:} raises TypeError on primitives

IO Binding in Config

Models, transforms, and losses can have their IO bound directly in the config using the io: key. This is one of the IO Binding methods:

model:
  _target: Identity
  params: {}
  io:
    inputs:
      input: x             # Model param 'input' reads from entry.x
    outputs: y              # Writes model output to entry.y

For transforms:

# EntryTransform — field keys in params, no io:
transform:
  _target: srforge.transform.entry.CopyFields
  params:
    field_key: y
    output_key: z

# DataTransform — param name maps to Entry field
transform:
  _target: srforge.transform.data.Multiply
  params:
    value: 2.0
  io:
    inputs:
      image: x
    outputs: y

The ConfigResolver handles this automatically — after creating the object, it calls instance.set_io(io_config) if the io: key is present. This works for any IOModule subclass (Models, Transforms, and Losses).

See IO Binding for all binding methods and detailed examples.


Full Config Structure

A complete training config has these top-level sections:

# ═══════════════════════════════════════════════
# System settings
# ═══════════════════════════════════════════════
system:
  cache_dir:              # Dataset cache directory (null = no caching)
  device: 0               # Single GPU by index; use [0, 1] for multi-GPU, or "cpu"
  recache: True           # Force re-cache even if cache exists
  mixed_precision: False  # Enable automatic mixed precision (AMP)
  debug_level: INFO       # Logging: DEBUG, INFO, WARNING, ERROR, CRITICAL

# ═══════════════════════════════════════════════
# Experiment tracking
# ═══════════════════════════════════════════════
tracker:
  _target: srforge.tracking.WandbTracker  # or srforge.tracking.NullTracker for offline runs
  params:
    mode: online              # online, offline, or disabled
    entity: my-team           # W&B team
    project: my-project       # W&B project name
    name:                     # Auto-generated if null
    run_id:                   # Set to resume a previous run
    group:                    # Group related runs
    tags:                     # List of tags
    notes:                    # Description
    job_type: training
    resume: allow

# ═══════════════════════════════════════════════
# Training hyperparameters
# ═══════════════════════════════════════════════
training:
  epochs: 1000
  batch_size: 8
  num_workers: 0
  gradient_accumulation_steps: 1

# ═══════════════════════════════════════════════
# Model
# ═══════════════════════════════════════════════
model:
  _target: MyModel
  params:
    hidden_dim: 128

# ═══════════════════════════════════════════════
# Optimizer
# ═══════════════════════════════════════════════
optimizer:
  _target: torch.optim.AdamW
  params:
    lr: 0.0005
    weight_decay: 0

# ═══════════════════════════════════════════════
# Learning rate scheduler (null = no scheduler)
# ═══════════════════════════════════════════════
lr_scheduler:
  _target: torch.optim.lr_scheduler.ReduceLROnPlateau
  params:
    mode: "min"
    factor: 0.5
    patience: 10

# ═══════════════════════════════════════════════
# Loss function
# ═══════════════════════════════════════════════
loss:
  _target: srforge.loss.combiner.LossCombiner
  params:
    losses:
      - _target: srforge.loss.metrics.L1
        params:
          weight: 1.0
        io:                        # map L1's x,y params to Entry fields
          inputs: {x: sr, y: hr}

# ═══════════════════════════════════════════════
# Validation metrics (often same as loss)
# ═══════════════════════════════════════════════
validation_metrics: ${loss}

# ═══════════════════════════════════════════════
# Datasets
# ═══════════════════════════════════════════════
dataset:
  training:
    _target: srforge.dataset.LazyDataset
    params:
      root: /path/to/train/data
  validation:
    _target: srforge.dataset.LazyDataset
    params:
      root: /path/to/val/data

# ═══════════════════════════════════════════════
# Pre- and post-processing (lists of transforms)
# ═══════════════════════════════════════════════
preprocessing:
  training:
    - _target: srforge.transform.data.ToFloat
      io:
        inputs: [{image: target}, {image: images}]
    - _target: srforge.transform.data.Divide
      params:
        value: 16383.0
      io:
        inputs: [{image: target}, {image: images}]
  validation: ${preprocessing.training}

postprocessing:
  training: []
  validation: ${postprocessing.training}

# ═══════════════════════════════════════════════
# Training runner (with its hooks)
# ═══════════════════════════════════════════════
training_runner:
  _target: srforge.training.runners.TrainingEpochRunner
  params:
    optimizer: ${ref:optimizer}                          # Reference to instantiated optimizer
    postprocessor: ${ref:postprocessing.training}       # Transforms applied after model output
    mixed_precision: ${system.mixed_precision}         # Raw config value (bool)
    gradient_accumulation_steps: ${training.gradient_accumulation_steps}
    hooks:
      - _target: srforge.training.hooks.ProgressBar    # Progress bar for the training loop
        params: { name: "T" }

# ═══════════════════════════════════════════════
# Validation runner (with its hooks)
# ═══════════════════════════════════════════════
validation_runner:
  _target: srforge.training.runners.ValidationEpochRunner
  params:
    postprocessor: ${ref:postprocessing.validation}
    mixed_precision: ${system.mixed_precision}
    hooks:
      - _target: srforge.training.hooks.ProgressBar    # Progress bar for the validation loop
        params: { name: "V" }
      - _target: srforge.training.hooks.BatchImageLogger   # Log sample predictions to tracker
        params:
          batch_id: 0
          img_key: sr
          log_dir: val_pred
          tracker: ${ref:tracker}

# ═══════════════════════════════════════════════
# Trainer (orchestrates the training loop; carries trainer-level hooks)
# ═══════════════════════════════════════════════
trainer:
  _target: srforge.training.trainers.PyTorchTrainer
  params:
    training_epoch_runner: ${ref:training_runner}         # References instantiated runner
    validation_epoch_runner: ${ref:validation_runner}
    training_criterion: ${ref:loss}                      # Loss function for training
    validation_criterion: ${ref:validation_metrics}      # Metrics for validation
    lr_scheduler: ${ref:lr_scheduler}
    # stop_condition:                                   # Optional early stopping
    #   _target: srforge.training.stop.ValidationLossDidNotImprove
    #   params:
    #     patience: 50
    #     min_delta: 0.0001
    hooks:
      - _target: srforge.training.hooks.LossLogger      # Log losses each epoch
        params: { tracker: ${ref:tracker} }
      - _target: srforge.training.hooks.PyTorchModelSaver   # Save checkpoints (best + last)
        params: { tracker: ${ref:tracker} }

Hooks attach where they fire

Notice there is no global observers: section and no scope: filters. A hook goes into the hooks: list of the component whose lifecycle it follows — runner-level hooks (progress bars, image loggers) on the runner, trainer-level hooks (loss logging, checkpointing) on the trainer. Attachment is the scope.


Trainer, Runners, and Hooks

The config sections above define three layers of the training architecture:

PyTorchTrainer

The trainer owns the epoch loop. Each epoch it:

  1. Calls training_runner.run_epoch() to train on the full dataset
  2. Calls validation_runner.run_epoch() to evaluate
  3. Steps the LR scheduler
  4. Checks the stop condition (if configured)

Parameters:

Parameter What It Does
training_epoch_runner Runner that performs the training pass
validation_epoch_runner Runner that performs the validation pass
training_criterion Loss function (or LossScheduler) for training
validation_criterion Loss/metrics for validation
lr_scheduler PyTorch LR scheduler (null = constant LR)
stop_condition Optional early stopping (e.g., ValidationLossDidNotImprove)

Runners

Runners execute a single pass through a dataset. Each runner moves data to the device, runs the model, applies postprocessing transforms, and computes loss/metrics.

Runner Use Case Key Params
TrainingEpochRunner Training with backprop optimizer, gradient_accumulation_steps, mixed_precision, postprocessor
ValidationEpochRunner Validation (no gradients) mixed_precision, postprocessor
BenchmarkRunner Testing/inference Same as validation, but criterion is optional

Hooks

Hooks are pluggable components that fire at defined points of the training lifecycle. Each hook goes in the hooks: list of the component whose lifecycle it follows:

Hook (srforge.training.hooks.*) Attach to What It Does
ProgressBar Runner CLI progress bar during the epoch
BatchImageLogger Runner Logs sample prediction images to the tracker
BatchImageSaver Runner Saves prediction images to disk
LossLogger Trainer Logs training/validation losses + LR to the tracker each epoch
PyTorchModelSaver Trainer Saves best and latest model checkpoints
GradientClip Runner Clips gradients after backward

There is no scope filter — a hook in the training runner's list fires only during training epochs; a hook in the validation runner's list fires only during validation. See Hooks for the full guide, including writing your own.

Legacy observers: sections

Configs written for older versions used a top-level observers: block with scope: filters, subscribed to a global EventBus by the script. That mechanism still works via a compatibility adapter but is deprecated — removal in v0.16.0. Migrate each observer into the appropriate hooks: list (the classes have Hook counterparts with the same names under srforge.training.hooks). See the porting guide.

Stop Conditions

The optional stop_condition on the trainer controls early stopping:

stop_condition:
  _target: srforge.training.stop.ValidationLossDidNotImprove
  params:
    patience: 50       # Epochs to wait for improvement
    min_delta: 0.0001  # Minimum change to count as improvement

If omitted, training runs for the full number of epochs.


How the Config Gets Used

The training script uses srforge.init() to create a ConfigResolver, then calls it to instantiate each component:

from srforge import GlobalSettings, init
from srforge.utils.checkpoint import resume_from_checkpoint

resolve = init(cfg)

# 1. Core components
model     = resolve(cfg.model)
loss      = resolve(cfg.loss)
optimizer = resolve(cfg.optimizer)
scheduler = resolve(cfg.lr_scheduler)

# 2. Datasets
train_dataset = resolve(cfg.dataset.training)
val_dataset   = resolve(cfg.dataset.validation)

# 3. Trainer — must exist before resume (resume needs the runner)
trainer = resolve(cfg.trainer)

# 4. Tracker
tracker = resolve(cfg.tracker)
tracker.log_config(omegaconf.OmegaConf.to_container(cfg, resolve=False))

# 5. Resume from checkpoint — pass the trainer's runner, not the
#    raw optimizer. Optimizer + scaler state live inside the runner.
ckpt = resume_from_checkpoint(
    model, trainer.training_runner, scheduler, tracker=tracker
)
trainer.restore(ckpt)  # None-safe: no-op for fresh runs

# 6. Train — hooks were already attached when the trainer/runners
#    were resolved (they live in the runners' and trainer's hooks: lists)
trainer.train(cfg.training.epochs, train_loader, val_loader)

tracker.finish(0)

The resolve() callable is the general-purpose instantiator. Pass any config subtree to it, and it will recursively instantiate whatever it finds. If the subtree has no _target key, it's returned as a plain dictionary (or list).

See Writing Scripts for a complete walkthrough.

Runtime Injection

Some values are not known at config time — they depend on runtime setup. These are passed as keyword arguments to resolve():

resolve(cfg.trainer, some_runtime_value=42)

The kwarg is forwarded to the constructor alongside the YAML-defined params.

Checkpoint state (epoch, best losses, scaler) is handled separately via trainer.restore(ckpt) — see Resuming Training.

Instance Caching

The resolver caches every instantiated object by its config path. This means:

# These return the same Python object
model1 = resolve(cfg.model)
model2 = resolve(cfg.model)
assert model1 is model2  # True — cached

This is critical for the ${ref:path} reference system to work correctly.

Caching edge cases

Extra kwargs are ignored on cache hit. If you call resolve(cfg.model, device="cuda") and later resolve(cfg.model, device="cpu"), the second call returns the cached instance from the first call — the device="cpu" argument is silently ignored.

${path} interpolation creates separate instances. See Which Should I Use? above.

Detached config nodes bypass caching. If you build a DictConfig manually (not from the resolver's config tree), resolve() cannot determine its path and will not cache the result. Calling it twice creates two separate instances.


CLI Overrides

SR-Forge uses Hydra for config loading, which gives you powerful CLI overrides for free:

# Change a single parameter
train model.params.hidden_dim=256

# Change multiple parameters
train training.batch_size=32 optimizer.params.lr=0.001

# Change the model entirely
train model._target=MyModel model.params.hidden_dim=128 model.params.num_layers=6

# Multi-GPU training
train 'system.device=[0,1]'

# Disable experiment tracking
train tracker.params.mode=disabled

The train and test commands are console entry points defined in pyproject.toml. They automatically load train-cfg.yaml or test-cfg.yaml respectively.


SequentialModel in Config

The SequentialModel can be fully defined in YAML, including all sub-models, transforms, and the flow DSL:

model:
  _target: srforge.models.SequentialModel
  params:
    modules:
      m1:
        _target: MyModel
        params: {}
      copy:
        _target: srforge.transform.entry.CopyFields
        params:
          field_key: y
          output_key: z
      mul:
        _target: srforge.transform.data.Multiply
        params:
          value: 2.0
    flow:
      - "x -> m1 -> y"                      # Flow DSL strings
      - " -> copy -> "                      # EntryTransform: opaque step
      - "z -> mul -> out"

Each module under modules: is recursively instantiated. Models and DataTransforms get their IO binding from the flow DSL. EntryTransforms are opaque steps — their field keys go in params:, and the flow line is just -> name ->. See SequentialModel for the full syntax.


Testing Config

The test config is simpler — no optimizer, scheduler, runners, or trainer:

system:
  device: 0
  output_dir: ./results       # Where to save output images

model:
  # Load a trained model from Weights & Biases
  _target: srforge.utils.model.get_model_from_wandb
  params:
    model_name: MyModel
    project: my-project
    entity: my-team
    run_id: abc123xyz          # W&B run to load from
    load_best_model: True      # Best checkpoint (vs. last)

test_metrics:
  _target: srforge.loss.combiner.LossCombiner
  params:
    losses:
      - _target: srforge.loss.metrics.PSNR
        params:
          weight: 1.0

dataset:
  _target: srforge.dataset.LazyDataset
  params:
    root: /path/to/test/data

postprocessing: []

Key differences from training:

  • No optimizer, lr_scheduler, training, or runner/trainer sections
  • Model is typically loaded from W&B rather than defined fresh
  • Single dataset (no training/validation split)
  • test_metrics instead of loss + validation_metrics

The test script creates a BenchmarkRunner — hooks (e.g. a progress bar or image saver) go in its hooks: list, in YAML or programmatically:

runner = BenchmarkRunner(metrics, device, postprocessor=postprocessor,
                         hooks=[ProgressBar(name="test")])
runner.run_epoch(model=model, data_loader=test_loader, epoch=0)

BenchmarkRunner works like ValidationEpochRunner but with an optional criterion — it can run pure inference without computing loss.

See Writing Scripts for complete training and test script examples.


Resuming Training

When the tracker detects a resumed run (tracker.is_resumed), the resume_from_checkpoint() utility handles everything:

  1. Downloads the last checkpoint from the tracker
  2. Restores runner state (optimizer + AMP scaler) via runner.load_training_state()
  3. Restores LR-scheduler state via lr_scheduler.load_state_dict()
  4. Loads model weights for all sub-models
  5. Restores RNG states for exact reproducibility
tracker:
  _target: srforge.tracking.WandbTracker
  params:
    run_id: abc123xyz    # Setting this triggers resume
    resume: allow
from srforge.utils.checkpoint import resume_from_checkpoint

# Create the trainer first — its `training_runner` owns the optimizer +
# AMP scaler state that resume_from_checkpoint needs to restore.
trainer = resolve(cfg.trainer)

ckpt = resume_from_checkpoint(
    model, trainer.training_runner, scheduler, tracker=tracker
)
trainer.restore(ckpt)  # None-safe: no-op for fresh runs

See Experiment Tracking for more details.


Common Patterns

Loss Scheduling

Change the loss function at specific epochs:

loss:
  _target: srforge.loss.schedule.LossScheduler
  params:
    schedule:
      0:                               # Epochs 0-49: L1 only
        _target: srforge.loss.metrics.L1
        params:
          weight: 1.0
      50:                              # Epochs 50+: L1 + SSIM
        _target: srforge.loss.combiner.LossCombiner
        params:
          losses:
            - _target: srforge.loss.metrics.L1
              params:
                weight: 1.0
            - _target: srforge.loss.metrics.SSIM
              params:
                weight: 0.05

Multi-GPU Training

system:
  device:
    - 0
    - 1

The DataLoaderFactory detects the list and configures distributed data loading automatically. The training script wraps the model in DataParallel.

Dataset Caching

system:
  cache_dir: /tmp/data_cache   # Set to null to disable
  recache: False               # True to force rebuild

When cache_dir is set, raw dataset entries are cached to disk as pickle files for fast subsequent loading.


Common Errors

Missing _target

# BAD — no _target, resolver doesn't know what to create
model:
  scale: 3
  filters: 32

Without _target, the resolver treats this as a plain dictionary, not an object to instantiate. It will pass it through as-is, which usually causes a TypeError downstream.

Class Not Found

ModuleNotFoundError: No module named 'srforge.models.MyModel'

Check:

  • Is the module path correct?
  • Is there a typo in _target?

IO Binding on Non-IOModule

TypeError: MyClass does not support io bindings but config provided 'io'.

Only IOModule subclasses (Models, Transforms, and Losses) support the io: key. Regular Python classes cannot have IO bindings.

Invalid Reference

KeyError: Config path 'model.backbone' not found.

The ${ref:path} must point to an existing config key. Check for typos in the reference path.


How It All Fits Together

train-cfg.yaml
  ├─ model, optimizer, loss, lr_scheduler, dataset
  ├─ preprocessing / postprocessing
  ├─ training_runner    (hooks: ProgressBar "T")
  ├─ validation_runner  (hooks: ProgressBar "V", BatchImageLogger)
  └─ trainer            (hooks: LossLogger, PyTorchModelSaver)
        ▼  resolve = init(cfg)
           resolve() instantiates all objects, resolves ${ref:...}
           hooks bind to their runner / trainer at construction
  PyTorchTrainer.train(epochs, train_loader, val_loader)
    ├─ Each epoch:
    │   ├─ TrainingEpochRunner.run_epoch(model, train_loader)
    │   │     ├─ Forward pass → postprocessor → loss → backward
    │   │     └─ its ProgressBar updates after each batch
    │   │
    │   ├─ ValidationEpochRunner.run_epoch(model, val_loader)
    │   │     ├─ Forward pass → postprocessor → metrics (no gradients)
    │   │     └─ its ProgressBar + BatchImageLogger update after each batch
    │   │
    │   ├─ LossLogger (trainer hook) logs train/val losses to tracker
    │   ├─ PyTorchModelSaver (trainer hook) saves best + last checkpoints
    │   ├─ LR scheduler steps
    │   └─ Stop condition checked
    └─ Training complete

Golden Rule: _target + params = the object you want. Nest them as deep as you need — the resolver handles the rest.


Summary

Concept What It Does
_target Specifies which class to instantiate
params Constructor arguments (keyword)
${path} Copies raw config values (OmegaConf interpolation)
${ref:path} References an already-instantiated Python object
io: key Binds IO for Models, Transforms, and Losses
init(cfg) Framework setup, returns a ConfigResolver
resolve(cfg.section) Instantiates any config section
resolve(cfg.section, key=val) Injects runtime-only values
resume_from_checkpoint(model, runner, lr_scheduler, tracker=) One-call checkpoint resume (runner owns optimizer+scaler)
hooks: list on runner/trainer params Attach hooks (progress bars, loggers, checkpointing)
PyTorchTrainer Orchestrates the epoch loop with runners
TrainingEpochRunner Runs training pass (forward, loss, backward, optimizer step)
ValidationEpochRunner Runs validation pass (forward, metrics, no gradients)
BenchmarkRunner Runs inference with optional metrics (used by test script)
CLI overrides train key=value changes any config parameter

Next: Experiment Tracking — Logging, checkpointing, and run resumption