OmegaConf Resolvers
FlexLock registers custom OmegaConf resolvers for dynamic configuration values.
Binding times
Config values are produced at four distinct moments. Match the mechanism to the moment:
Binding time |
Mechanism |
|---|---|
submit-time |
plain interpolation ( |
submit-time |
|
|
|
stage-start |
deferred resolvers |
per-sweep-item |
the sweep item is merged before resolution, so |
save_dir_policy (replaces ${vinc:} / ${now:})
Configs keep a stable save_dir. The policy decides what happens when that
directory is already occupied — i.e. contains a run.lock from a previous
run (run.complete marks it finished; run.lock alone means crashed or
in-flight):
policy |
behaviour when |
|---|---|
|
refuse: raise |
|
version the base ( |
|
delete the dir’s contents, then run (never touches a dir without |
|
don’t execute; return the existing complete run’s result (raises if the occupant is incomplete) |
|
run in place, no check (pre-0.8 behaviour) |
|
|
proj.submit(cfg) # raises if save_dir holds a run
proj.submit(cfg, force=True) # explicit in-place rerun
proj.submit(cfg, save_dir_policy="increment") # fresh versioned dir every run
flexlock-run ... --save-dir-policy increment
flexlock-run ... --force # rerun a crashed/stale run in place
Interaction rules:
smart_runwins first: a matching complete run is returned from cache before the guard fires — the guard only sees genuine conflicts (crashed runs, changed configs,smart_run=False).force=Truebypasses the default guard (it invalidatesrun.completeand reruns in place).Sweeps: a naming policy applies once to the sweep root; the guard is enforced per item after the per-item cache check.
"skip"on a sweep means resume: complete items are reused, crashed items rerun in place.The policy is applied after
print_config/--check/dry_run, which therefore never create or delete anything.
Migration
Old (deprecated) |
New |
|---|---|
|
|
|
|
${vinc:} and ${now:} still work for one deprecation cycle but emit a
DeprecationWarning.
Available Resolvers
${now:format} - Current Timestamp
Deprecated. Prefer
save_dir_policy="timestamp". See migration above.
Get current timestamp in specified format.
Usage:
_target_: myproject.train
save_dir: outputs/train_${now:%Y%m%d_%H%M%S}
Python:
cfg = py2cfg(
train,
save_dir='outputs/train_${now:%Y%m%d_%H%M%S}'
)
Format strings:
%Y: 4-digit year (2024)%m: 2-digit month (01-12)%d: 2-digit day (01-31)%H: 2-digit hour (00-23)%M: 2-digit minute (00-59)%S: 2-digit second (00-59)
Examples:
timestamp: ${now:%Y-%m-%d %H:%M:%S} # "2024-01-15 14:30:00"
run_id: ${now:%y%m%d%H%M} # "2401151430"
date: ${now:%Y%m%d} # "20240115"
${run:preset,select[,pin][,strict]} - Newest Run of a Preset
The directory of the newest complete run of a preset (the -d target +
-s key it was launched with; see the usage guide, §7 “Presets”). Use it
instead of copying a versioned path such as results/train_x_0005.
infer = dict(
train_dir="${run:xps_glob.train_small_cloud_gap,main}",
main=py2cfg(infer_fn, xp_path="${train_dir}",
checkpoint="${train_dir}/checkpoints/best_model.ckpt", ...),
)
# pin a version (the run whose dir name, or path, ends with the pin)
flexlock-run -d my.xps.infer -s main -o 'train_dir=${run:xps_glob.train_x,main,0005}'
# only runs launched without -o/-O/-m/-M overrides
flexlock-run -d my.xps.infer -s main -o 'train_dir=${run:xps_glob.train_x,main,strict}'
Behavior:
Any run of the preset matches, whatever its overrides (use
strict, or a separate preset such astrain_x_debug, to keep quick tests out).“Newest” means most recently completed; failed, interrupted and running runs are skipped. The preset address matches by suffix (
xps_glob.train_x,xps_glob:train_x,train_x).Resolved once, at submit (and by
--dump,--enqueue,--print-config): the concrete path is what the config, the task DB andrun.lockrecord, so a job waiting in the queue can’t pick a newer run. The choice is logged:${run:xps_glob.train_x,main} → results/train_x_0005.The resolved run is added to
_snapshot_.prevs, so lineage (flexlock show/graph) records it without a hand-written entry.No match is an error that lists the preset’s complete runs (or similarly named presets). Without a
select, runs of several-skeys are an error.Only values containing
${run:are resolved early; other interpolations in the config (e.g.${.save_dir}/logs) keep their usual binding time.
${latest:glob} - Find Latest Path
Find the most recently modified path matching a glob pattern.
Usage:
_target_: myproject.eval
model_dir: ${latest:outputs/train_*/}
Python:
cfg = py2cfg(
evaluate,
model_dir='${latest:outputs/train_*}'
)
Behavior:
Expands glob pattern (uses
pathlib.Path.glob())Sorts by modification time (most recent first)
Returns string path of latest match
Raises error if no matches found
Examples:
# Latest checkpoint
checkpoint: ${latest:checkpoints/epoch_*.pth}
# Latest experiment run
prev_run: ${latest:outputs/experiments/run_*/}
# Latest data file
data_file: ${latest:data/processed_*.csv}
${vinc:path} - Version Increment
Deprecated. Prefer
save_dir_policy="increment", which also claims the directory atomically. See migration above.
Generate next version number for a directory path.
Usage:
_target_: myproject.train
save_dir: ${vinc:outputs/pipeline/run}
Behavior:
Searches for existing directories matching
{path}_*Finds highest numeric suffix
Returns
{path}_{n+1}where n is the highest existing
Examples:
Existing directories:
outputs/pipeline/
run_0001/
run_0002/
run_0005/
Config:
save_dir: ${vinc:outputs/pipeline/run}
Resolves to:
outputs/pipeline/run_0006/
Use cases:
Auto-versioned experiment directories
Sequential pipeline runs
Avoid overwriting previous results
${run_lock:run_dir,key} - Read from Upstream Run
Read a field from an upstream run’s run.lock file using a dot-separated key path. Essential for downstream stages that need config values from a training or preprocessing run.
Usage:
run_dir: results/train_0005
stats_file: ${run_lock:${run_dir},config.datamodule.stats_file}
checkpoint: ${run_lock:${run_dir},config.save_dir}/checkpoints/best.ckpt
With default (optional third argument):
# Returns "none" if the key doesn't exist
optional_field: ${run_lock:${run_dir},config.flow_checkpoint,none}
Python config example — downstream inference from upstream training:
cfg = dict(
run_dir="results/train_0005",
main=py2cfg(inference,
stats_file="${run_lock:${run_dir},config.datamodule.stats_file}",
checkpoint="${run_lock:${run_dir},config.save_dir}/checkpoints/best.ckpt",
prepared_data_dir="${run_lock:${run_dir},config.datamodule.prepared_data_dir}",
# Override run_dir with -o to point to a different upstream run
)
)
Override the upstream run from CLI:
flexlock-run -d myproject.inference_cfg -s main \
-o run_dir=results/train_0008
Behavior:
Reads
run_dir/run.lockand navigates the dot-path (e.g.,config.datamodule.stats_file)Supports nested keys to any depth
Optional third argument provides a default if the key is missing or null
Raises
FileNotFoundErrorifrun.lockdoesn’t exist (unless default given)Raises
KeyErrorif key path doesn’t exist (unless default given)Not cached (
use_cache=False) — re-reads on each resolution, so changingrun_dirvia-oworks correctly
Template configs with a single anchor
Put one top-level variable in your defaults dict. All downstream
templates derive their input paths from it via ${run_lock:}:
defaults = dict(
cnf_run_dir="${latest:outputs/cnf_train/run_*}",
eval=py2cfg(evaluate,
data_dir="${run_lock:${cnf_run_dir},config.data_dir}",
model_dir="${run_lock:${cnf_run_dir},config.save_dir}",
save_dir="outputs/eval/run", # + --save-dir-policy increment
),
)
Override from CLI: flexlock-run -d defaults -s eval -o cnf_run_dir=outputs/cnf_train/run_0003
Override from API: OmegaConf.update(proj.cfg, 'cnf_run_dir', result.save_dir)
Using Resolvers
In YAML Configs
# config.yaml
_target_: myproject.train
# Dynamic timestamp
save_dir: outputs/train_${now:%Y%m%d_%H%M%S}
# Latest model
pretrained_model: ${latest:models/pretrained_*.pth}
# Versioned output
results_dir: ${vinc:outputs/results/exp}
In Python Configs
from flexlock import py2cfg
from omegaconf import OmegaConf
cfg = py2cfg(
train,
save_dir='outputs/train_${now:%Y%m%d_%H%M%S}',
input_data='data/train.csv'
)
# Resolve all interpolations
resolved = OmegaConf.to_container(cfg, resolve=True)
print(resolved['save_dir']) # "outputs/train_20240115_143000"
In Decorator
from flexlock import flexcli, py2cfg
@flexcli(
save_dir='outputs/train_${now:%Y%m%d_%H%M%S}',
snapshot_config=dict(
repos={'main': '.'},
data={'input': '${...input_path}'}
)
)
def train(input_path, save_dir=None):
print(f"Saving to {save_dir}") # Auto-resolved
Advanced Usage
Nested Resolvers
# Use resolvers in paths
checkpoint: ${latest:${base_dir}/checkpoints/epoch_*.pth}
# Combine timestamp and versioning
save_dir: ${vinc:outputs/train_${now:%Y%m%d}/run}
Conditional Logic
# Use OmegaConf's oc.select for conditionals
data_path: ${oc.select:custom_data_path,${latest:data/default_*.csv}}
Pipeline Dependencies
# Stage 1: Preprocess
preprocess:
_target_: myproject.preprocess
save_dir: outputs/preprocess/run
# Stage 2: Train (depends on Stage 1)
train:
_target_: myproject.train
preprocess_dir: ${latest:outputs/preprocess/run_*/}
save_dir: outputs/train/run
Run each stage with --save-dir-policy increment (or rely on the default
guard when a stage should run exactly once per directory).
Execution:
proj = Project(defaults='pipeline.defaults')
# Run preprocess
prep_result = proj.submit(proj.get('preprocess'))
# Train automatically finds latest preprocess output
train_result = proj.submit(proj.get('train'))
Custom Resolvers
Register your own resolvers:
from omegaconf import OmegaConf
def my_resolver(value: str) -> str:
return value.upper()
# Register resolver
OmegaConf.register_new_resolver("upper", my_resolver)
# Use in config
cfg = OmegaConf.create({
"name": "model",
"NAME": "${upper:${name}}"
})
print(cfg.NAME) # "MODEL"
Examples:
# Environment variable resolver
import os
OmegaConf.register_new_resolver("env", lambda var: os.getenv(var))
# Usage: ${env:HOME}
# Math resolver
OmegaConf.register_new_resolver("mul", lambda a, b: float(a) * float(b))
# Usage: warmup_steps: ${mul:${total_steps},0.1}
# Path join resolver
from pathlib import Path
OmegaConf.register_new_resolver(
"join",
lambda *parts: str(Path(*parts))
)
# Usage: model_path: ${join:${base_dir},models,best.pth}
Best Practices
1. Use save_dir_policy="increment" for Sequential Runs
# Good: Auto-versioning, atomic claim, no resolver side effects
cfg = py2cfg(train, save_dir='outputs/exp/run')
proj.submit(cfg, save_dir_policy="increment")
# Bad: Manual versioning (error-prone)
cfg = py2cfg(train, save_dir='outputs/exp/run_0042')
2. Reference upstream runs by preset, not by path
# Best: the newest complete run of the upstream preset, recorded at submit
train_cfg = py2cfg(train, preprocess_dir='${run:xps.preprocess,main}')
# Good: Automatically finds latest upstream by glob (mtime, any run)
train_cfg = py2cfg(
train,
preprocess_dir='${latest:outputs/preprocess/run_*}'
)
# Bad: Hardcoded path (breaks when preprocess reruns)
train_cfg = py2cfg(train, preprocess_dir='outputs/preprocess/run_0001')
3. Combine Resolvers for Powerful Patterns
# Latest data file reference
input_data: ${latest:data/processed_*.csv}
# Pipeline stage with dependency
upstream_dir: ${latest:outputs/stage1/run_*/}
4. Track Data with Snapshot Config
For data tracking, use _snapshot_ in py2cfg (or snapshot_config when using the @flexcli decorator):
cfg = py2cfg(
train,
input_data='data/train.csv',
_snapshot_=dict(
repos={'main': '.'},
data={'train': '${...input_data}'}
)
)
Note: Use
_snapshot_=insidepy2cfg. Thesnapshot_config=spelling only works as a parameter to the@flexclidecorator — passing it topy2cfgwill forward it as an unexpected keyword argument to the function.
See Also
OmegaConf Documentation - Interpolation syntax
Python API - Using resolvers programmatically
CLI Reference - Resolver usage in CLI workflows