Configuration Indirection: _target and ${ref:...}¶
This page is a focused tour of the two runtime-indirection mechanisms
that compose every SR-Forge experiment: _target dispatch (turn a
YAML string into a Python class) and ${ref:...} references (point
one config block at another). For the wider configuration system
walk-through see Configuration; for binding model
inputs / outputs see IO Binding.
The mechanisms are intentionally string-based at the YAML layer so the
schema stays declarative and editable without code. The cost is that
static-analysis tools (linters, IDE go-to-definition, code-graph
indexers) cannot follow _target: "..." into the class definition — the
connection only exists once ConfigResolver runs.
To make these indirection sites discoverable from the codebase itself, SR-Forge exposes them as first-class types you can import:
| Concept | Canonical type | Module |
|---|---|---|
_target dispatch |
TargetSpec |
srforge.config.spec |
${ref:...} reference |
Reference |
srforge.config.spec |
| Static audit of a YAML | audit_config |
srforge.config.audit |
ConfigResolver itself simply delegates to these primitives — both the
runtime dispatch and the (separate) static audit tool share the same
parser.
_target¶
A YAML block with a _target key tells ConfigResolver to instantiate
a class. The string value is either a fully-qualified Python path or a
short name registered in ClassRegistry.
model:
_target: srforge.models.MISR.HighResNet # fully-qualified
params:
in_channels: 1
scale: 3
io:
inputs: {lrs: lrs, alphas: alphas}
outputs: sr
At resolve time the framework does
importlib.import_module(module).getattr(class_name) and calls the
class with params. The io block (when present) is applied
post-construction via IOModule.set_io.
TargetSpec is the parsed-but-not-yet-instantiated view of that
block:
from srforge.config.spec import TargetSpec
spec = TargetSpec.from_node(model_yaml_node)
spec.target # "srforge.models.MISR.HighResNet"
spec.params # {"in_channels": 1, "scale": 3}
spec.io # {"inputs": {...}, "outputs": "sr"}
# Static lookup — no instantiation:
module, class_name = spec.resolve_class()
# ("srforge.models.MISR", "HighResNet")
# Or pull the actual class object:
cls = spec.import_class()
This is the same lookup ConfigResolver._instantiate_target performs —
splitting the work into a dataclass means inspection code (and static
analyzers) can follow the indirection without pulling in the rest of
the resolver pipeline.
${ref:...}¶
A ${ref:path} interpolation tells ConfigResolver to reuse an
already-resolved object from elsewhere in the same config tree.
Optional zero-argument method chains are allowed.
optimizer:
_target: torch.optim.Adam
params:
params: ${ref:model}.trainable_params() # reuse model, call method
lr: 1e-3
lr_scheduler:
_target: torch.optim.lr_scheduler.StepLR
params:
optimizer: ${ref:optimizer} # reuse optimizer
${ref:foo} is an OmegaConf custom resolver registered by SR-Forge that
emits an internal %{foo} marker; ConfigResolver._resolve_reference_string
recognises both forms.
Reference is the parsed-but-not-yet-resolved view of that marker:
from srforge.config.spec import Reference
Reference.parse("%{model}")
# Reference(path="model", methods=())
Reference.parse("%{model}.trainable_params()")
# Reference(path="model", methods=("trainable_params",))
The path is a dotted config-tree path (a.b.c or a.b[0].c —
bracket notation is normalised at lookup time). Methods are restricted
to zero-argument calls; argument-passing inside a reference is
intentionally not supported.
The static graph: ConfigGraph¶
srforge.config.spec.ConfigGraph is the internal data structure that
powers the audit. Every ConfigResolver builds one in its
__init__ (so the audit runs on every script startup as part of
init()'s pre-flight check), then queries it lazily as each
sub-config is resolved.
Each node in the graph carries:
| Field | Meaning |
|---|---|
path |
Dotted config path (e.g. loss.params.bands.params.b) |
kind |
target / reference / mapping / sequence / primitive |
spec |
The TargetSpec (for target nodes) or Reference (for reference nodes), None otherwise |
children |
Paths of child nodes in the dependency sense — only the params subtree of a target is followed; io and _target aren't graph children |
Build it directly (rare — normally you let ConfigResolver do it):
from srforge.config.spec import ConfigGraph
from omegaconf import OmegaConf
cfg = OmegaConf.load("configs/train-cfg.yaml")
graph = ConfigGraph.from_config(cfg)
edges = graph.to_edges() # list[IndirectionEdge]
from_config raises ValueError if a ${ref:...} points at a
non-existent path or if the references form a cycle. Both checks are
purely structural — no class is imported, no constructor is called.
ConfigResolver.audit()¶
Already-instantiated resolvers expose the same data without re-walking the config:
from srforge import init
resolve = init(cfg)
edges = resolve.audit() # list[IndirectionEdge] from the precomputed graph
This is what srforge audit <path> (see Writing Scripts § The
srforge CLI) calls under the hood, with
the rendering done by edges_to_text / edges_to_json /
edges_to_graphml.
Static audit: audit_config¶
srforge.config.audit.audit_config is the file-path entry point for
the same audit. It loads the YAML, instantiates a ConfigResolver
(which builds the ConfigGraph and validates it), then returns the
edge list. Useful for:
- Documenting which classes a given experiment depends on.
- Generating sidecar graphs that code-graph tools (Graphify, Sourcegraph, Neo4j) can merge into a wider knowledge graph.
- Pre-flight checking that every
_targetis importable before a long training run starts.
Cycle / dangling-reference detection runs first
Because audit_config constructs a ConfigResolver, the graph's
validation (cycle detection, missing-reference detection) runs
before the edges are returned. A broken config raises
ValueError rather than yielding a partial edge list. If you
want best-effort output from a broken config, wrap the call:
from srforge.config.audit import audit_config, edges_to_json
edges = audit_config("configs/inference/hrnet_mus2.yaml")
print(edges_to_json(edges))
Or via the CLI:
srforge audit configs/inference/hrnet_mus2.yaml
srforge audit configs/inference/hrnet_mus2.yaml --format json
srforge audit configs/inference/hrnet_mus2.yaml --format graphml > hrnet.graphml
Each edge is an IndirectionEdge:
@dataclass(frozen=True)
class IndirectionEdge:
kind: Literal["target", "reference"]
source_path: str # dotted config-tree path
raw: str # literal string from the YAML
resolved: str | None # "module.Class" or referenced path
resolved is None when the audit cannot deterministically follow the
indirection — e.g. an unregistered short-name _target. The raw
string is preserved so tools can still surface the edge for human
review.
Worked example¶
configs/inference/hrnet_mus2.yaml produces 22 edges, including:
source_path |
kind |
raw |
resolved |
|---|---|---|---|
model |
target | srforge.utils.checkpoint.load_weights_from_wandb |
srforge.utils.checkpoint.load_weights_from_wandb |
model.params.module |
target | srforge.models.MISR.HighResNet |
srforge.models.MISR.HighResNet |
dataset.params.transforms.0 |
target | srforge.transform.entry.CalculateTranslations |
srforge.transform.entry.CalculateTranslations |
dataset.params.transforms.6 |
target | srforge.transform.entry.PadAlongAxes |
srforge.transform.entry.PadAlongAxes |
In a code-graph tool that ingests this manifest, each resolved value
becomes a node, each source_path becomes a node, and the edges
between them make the previously-invisible string-to-class indirection
queryable alongside the rest of the codebase structure.
See also¶
- Configuration — full configuration walk-through.
- IO Binding — what the
io:block under a_targetactually does. srforge.config.spec— source forTargetSpecandReference.srforge.config.audit— source foraudit_configand the exporters.