Pipeline Configs
Prerequisites: 01.why-laco.ipynb through 06.nested-configs-and-containers.ipynb.
Dependencies: torch, torchvision. Real MNIST download cells are marked. Skip or mock them if offline.
The previous notebooks focused on a single component: one model, one optimizer, one schema. Real training pipelines wire many such components together: model, optimizer, loss function, dataset, and data loader, all addressable from a single config file.
This notebook introduces:
- The progression from a single-component config to a full pipeline bundle.
L.Dict: the pipeline root that groups named components.- Relative imports between config files.
- The complete
pipelines/mnist_train.pyexample annotated line by line. - Override grammar for pipeline configs.
- A composition diagram showing how source files relate.
- The
__file__-loading pattern used by runnable pipeline modules.
import laco
import laco.language as L
from omegaconf import OmegaConf
from torch import nn, optim
from torch.utils.data import DataLoader
Section 1: From Single-Component to Pipeline
This section traces the natural progression. Each step exposes a new need.
# ============================================================
# Step 1 — A single model config
# ============================================================
model_cfg = laco.load("configs://examples/typed/mlp.py#model")
print("=== Single model ===")
# We have a recipe for one component — great.
print(list(OmegaConf.to_container(model_cfg, resolve=False).keys())[:3], "...")
=== Single model ===
['_target_', '_args_', '_convert_'] ...
# ============================================================
# Step 2 — Model + optimizer as two separate objects
# ============================================================
from laco.examples.typed.linear_regression import (
model as lr_model_cfg,
optimizer as lr_optim_cfg,
OptimGroup,
)
# The two exports are *different kinds* of node:
# - model : a fully-formed DictConfig recipe (it has a _target_)
# - optimizer : L.chosen(OptimGroup) — a *slot reference* into a typed
# group (an interpolation like '${optimgroup}'), not a
# standalone recipe, so it has no _target_ of its own.
print("model type :", type(lr_model_cfg).__name__)
print("model _target_ :", lr_model_cfg._target_) # type: ignore[union-attr] # noqa: LACO001
print("optimizer type :", type(lr_optim_cfg).__name__)
print("optimizer ref :", str(lr_optim_cfg))
print("chosen variant :", OptimGroup.sgd._target_) # what the slot resolves to
# Problem: they are two separate objects with no shared namespace.
# A training loop would call laco.load() twice, with different fragment paths.
# There is no way to load *all components at once* or serialize the whole pipeline.
model type : DictConfig
model _target_ : torch.nn.Linear
optimizer type : _SlotRef
optimizer ref : ${optimgroup}
chosen variant : torch.optim.SGD
# ============================================================
# Step 3 — Bundle them into one tree
# ============================================================
# The model recipe references '${schema.*}' interpolations, and the optimizer
# is a group slot. To dump/instantiate a *self-contained* bundle, gather the
# components together with a concrete `schema` node so the references resolve
# (laco.load does this bundling for you when it reads a whole file; here we do
# it by hand for an isolated fragment).
from laco.examples.typed.linear_regression import (
model as lr_model_cfg,
OptimGroup,
)
train_bundle = OmegaConf.create({
"schema": {"in_features": 4, "out_features": 1, "bias": True},
"model": lr_model_cfg,
"optimizer": OptimGroup.sgd, # the concrete optimizer the slot resolves to
})
print("Bundle keys:", list(OmegaConf.to_container(train_bundle, resolve=False).keys()))
print("\n--- bundle dump (schema lets the model's interpolations resolve) ---")
print(laco.dump(train_bundle))
Bundle keys: ['schema', 'model', 'optimizer']
--- bundle dump (schema lets the model's interpolations resolve) ---
_laco_: 1
model: {_convert_: all, _target_: torch.nn.Linear, bias: '${schema.bias}', in_features: '${schema.in_features}',
out_features: '${schema.out_features}'}
optimizer: {_convert_: all, _partial_: true, _target_: torch.optim.SGD, lr: 0.01,
momentum: 0.9}
schema: {bias: true, in_features: 4, out_features: 1}
L.Dict is a plain DictConfig wrapper: it does not add any _target_ key of its
own. After laco.load("pipeline.py#train") the bundle config can be inspected,
serialized, or passed to a training loop that calls laco.instantiate(cfg.model),
laco.instantiate(cfg.optimizer, model.parameters()), etc.
Section 2: L.Dict, The Pipeline Root
L.Dict accepts keyword arguments whose values are config nodes and produces a single
DictConfig that holds all of them under their keyword names. This lets a downstream
caller get all components from a single fragment:
# Build a minimal pipeline bundle inline (no actual MNIST)
model_cfg = L.call(nn.Linear)(in_features=784, out_features=10)
optimizer_cfg = L.partial(optim.Adam)(lr=1e-3)
loss_cfg = L.call(nn.CrossEntropyLoss)()
pipeline = L.Dict(
model=model_cfg,
optimizer=optimizer_cfg,
loss=loss_cfg,
)
print("Top-level keys:", list(OmegaConf.to_container(pipeline, resolve=False).keys()))
# Each sub-node is a fully self-contained DictConfig recipe
print("\nmodel node:")
print(laco.dump(pipeline.model)) # type: ignore[union-attr] # noqa: LACO001
Top-level keys: ['_target_', '_convert_', 'model', 'optimizer', 'loss']
model node:
{_convert_: all, _laco_: 1, _target_: torch.nn.Linear, in_features: 784, out_features: 10}
# Instantiate only what you need — no need to materialise the whole pipeline
model_obj = laco.instantiate(pipeline.model) # type: ignore[union-attr] # noqa: LACO001
loss_obj = laco.instantiate(pipeline.loss) # type: ignore[union-attr] # noqa: LACO001
print("model:", model_obj)
print("loss: ", loss_obj)
model: Linear(in_features=784, out_features=10, bias=True)
loss: CrossEntropyLoss()
The #train fragment pattern
In a real config file you expose the bundle under a stable name:
# pipeline.py
model = L.call(MyModel)(...)
optimizer = L.partial(optim.Adam)(lr=1e-3)
loss = L.call(nn.CrossEntropyLoss)()
train = L.Dict(model=model, optimizer=optimizer, loss=loss)
Then a caller loads laco.load("pipeline.py#train") and gets a single DictConfig
containing all three components. Individual components are still accessible via
laco.load("pipeline.py#model"). The two access patterns coexist.
Section 3: Relative Imports in Config Files
Config files are regular Python modules. This means they can import from each other
using the standard from package.module import name pattern.
The MNIST pipeline imports a factory from the CNN classifier example:
# The import at the top of mnist_train.py:
#
# from laco.examples.cnn_classifier import make_cnn_classifier
#
# This imports a Python *function* (not a DictConfig node).
# The function returns a DictConfig tree when called, so calling it inside
# the pipeline file is like inlining the config construction.
from laco.examples.cnn_classifier import make_cnn_classifier
# Calling the factory with explicit arguments returns a DictConfig tree
cnn_cfg = make_cnn_classifier(
in_channels=1,
base_channels=16,
num_stages=2,
num_classes=10,
)
print("type:", type(cnn_cfg))
print("_target_:", cnn_cfg._target_) # noqa: LACO001
type: <class 'omegaconf.dictconfig.DictConfig'>
_target_: torch.nn.Sequential
Why a factory function instead of a module-level model attribute?
The cnn_classifier.py module exposes both:
model: a pre-built DictConfig node with defaulthpsvalues.make_cnn_classifier(**kwargs): a factory that accepts explicit values.
The pipeline uses the factory because it needs to pass values from its own @L.params class hps, which are interpolation references, not literal integers. A factory
function called with those references builds the DictConfig tree with the right
interpolation strings already in place.
# How the pipeline threads interpolations through the factory:
@L.params
class pipeline_hps:
in_channels: int = 1
base_channels: int = 32
num_stages: int = 3
num_classes: int = 10
# pipeline_hps.in_channels is NOT the integer 1 —
# it is the interpolation string '${pipeline_hps.in_channels}'
# (or however laco names the params namespace)
model_with_refs = make_cnn_classifier(
in_channels=pipeline_hps.in_channels,
base_channels=pipeline_hps.base_channels,
num_stages=pipeline_hps.num_stages,
num_classes=pipeline_hps.num_classes,
)
# The resulting DictConfig stores the interpolations, not literal values.
# To resolve them we bundle the params node alongside the model (calling
# pipeline_hps() materialises the namespace as a plain dict). Overriding
# pipeline_hps.num_classes=5 on this bundle would then propagate everywhere.
bundle = OmegaConf.create({"pipeline_hps": pipeline_hps(), "model": model_with_refs})
head_lines = [l for l in laco.dump(bundle).splitlines() if "num_classes" in l]
print("head out_features references:", head_lines)
head out_features references: [" out_features: '${pipeline_hps.num_classes}'}", 'pipeline_hps: {base_channels: 32, in_channels: 1, num_classes: 10, num_stages: 3}']
Section 4: Full Walkthrough, pipelines/mnist_train.py
This section reads through the complete pipeline config, annotating every section. The
source is at sources/laco/examples/pipelines/mnist_train.py.
# ============================================================
# Section A — Hyperparameters
# ============================================================
# @L.params creates a DictConfig namespace whose attributes are interpolation
# strings. Every other config node in the file can reference these via
# '${hps.batch_size}' etc.
from torchvision.datasets import MNIST
from torchvision import transforms
@L.params
class hps:
data_root: str = "./data"
batch_size: int = 64
learning_rate: float = 1e-3
num_workers: int = 0
# Architecture hyperparameters — shared with the model factory
in_channels: int = 1
base_channels: int = 32
num_stages: int = 3
num_classes: int = 10
print("hps type:", type(hps))
print("hps.batch_size (runtime):", repr(hps.batch_size)) # noqa: LACO001
hps type: <class 'laco.language.ParamsWrapper'>
hps.batch_size (runtime): '${hps.batch_size}'
# ============================================================
# Section B — Model
# ============================================================
# make_cnn_classifier is called with interpolation references.
# The factory returns a DictConfig tree where every architecture
# dimension is a '${hps.*}' string — overriding hps.num_stages=2
# at load time changes the depth of the CNN without re-running this file.
model = make_cnn_classifier(
in_channels=hps.in_channels,
base_channels=hps.base_channels,
num_stages=hps.num_stages,
num_classes=hps.num_classes,
)
print("model _target_:", model._target_) # noqa: LACO001
model _target_: torch.nn.Sequential
# ============================================================
# Section C — Optimizer and loss
# ============================================================
# L.partial produces a "partial constructor" node (_partial_: true).
# At instantiation time you still pass model.parameters() — the optimizer
# cannot be fully instantiated without the model weights.
optimizer = L.partial(optim.Adam)(lr=hps.learning_rate)
# L.call produces a full constructor node — CrossEntropyLoss takes no
# required arguments, so it can be instantiated directly.
loss = L.call(nn.CrossEntropyLoss)()
print("optimizer _partial_:", optimizer._partial_) # noqa: LACO001
print("loss _target_ :", loss._target_) # noqa: LACO001
optimizer _partial_: True
loss _target_ : torch.nn.CrossEntropyLoss
# ============================================================
# Section D — Data pipeline
# ============================================================
# L.List is a DictConfig list wrapper — it holds an ordered sequence of
# config nodes. transforms.Compose accepts a list of transforms, so we
# build the entire transform pipeline as a config tree.
_transform = L.call(transforms.Compose)(
L.List(
L.call(transforms.ToTensor)(),
L.call(transforms.Normalize)(
mean=L.List(0.1307), # single-channel mean
std=L.List(0.3081), # single-channel std
),
)
)
print("transform _target_:", _transform._target_) # noqa: LACO001
# Dataset: MNIST with root, split, download, and the transform node above.
# 'download=True' is a literal bool — not an interpolation; it is part of the recipe.
dataset = L.call(MNIST)(
root=hps.data_root,
train=True,
download=True,
transform=_transform,
)
# DataLoader wraps the dataset — note that 'dataset=' receives the DictConfig
# recipe for MNIST, not an instantiated Dataset object. Hydra will
# recursively instantiate nested nodes automatically.
loader = L.call(DataLoader)(
dataset=dataset,
batch_size=hps.batch_size,
shuffle=True,
num_workers=hps.num_workers,
)
print("loader _target_ :", loader._target_) # noqa: LACO001
print("dataset _target_ :", loader.dataset._target_) # noqa: LACO001 # nested!
transform _target_: torchvision.transforms.Compose
loader _target_ : torch.utils.data.DataLoader
dataset _target_ : torchvision.datasets.MNIST
# ============================================================
# Section E — The train bundle
# ============================================================
# L.Dict collects all components under a single DictConfig root.
# This is the fragment a training script loads with:
# cfg = laco.load("configs://examples/pipelines/mnist_train.py#train")
train = L.Dict(
model=model,
optimizer=optimizer,
loss=loss,
loader=loader,
)
print("train bundle keys:", list(OmegaConf.to_container(train, resolve=False).keys()))
train bundle keys: ['_target_', '_convert_', 'model', 'optimizer', 'loss', 'loader']
# Load the actual file from the examples package (does not instantiate anything)
full_cfg = laco.load("configs://examples/pipelines/mnist_train.py")
print("Top-level fragments:", list(OmegaConf.to_container(full_cfg, resolve=False).keys()))
Top-level fragments: ['hps', 'model', 'optimizer', 'loss', 'dataset', 'loader', 'train']
# Load just the train bundle fragment
train_cfg = laco.load("configs://examples/pipelines/mnist_train.py#train")
print("train bundle keys:", list(OmegaConf.to_container(train_cfg, resolve=False).keys()))
print()
# model and optimizer are immediately available as sub-configs
print("=== model ===")
print(laco.dump(train_cfg.model)) # type: ignore[union-attr] # noqa: LACO001
train bundle keys: ['_target_', '_convert_', 'model', 'optimizer', 'loss', 'loader']
=== model ===
_args_:
- _convert_: all
_target_: laco.language.OrderedDict.target
items:
- - stem
- _args_:
- {_convert_: all, _target_: torch.nn.Conv2d, bias: false, in_channels: '${hps.in_channels}',
kernel_size: 3, out_channels: '${hps.base_channels}', padding: 1}
- {_convert_: all, _target_: torch.nn.BatchNorm2d, num_features: '${hps.base_channels}'}
- {_convert_: all, _target_: torch.nn.ReLU, inplace: true}
_convert_: all
_target_: torch.nn.Sequential
- - stages
- _args_:
_convert_: all
_target_: laco.ops.repeat
num: ${hps.num_stages}
src:
_args_:
- {_convert_: all, _target_: torch.nn.Conv2d, bias: false, in_channels: '${hps.base_channels}',
kernel_size: 3, out_channels: '${hps.base_channels}', padding: 1}
- {_convert_: all, _target_: torch.nn.BatchNorm2d, num_features: '${hps.base_channels}'}
- {_convert_: all, _target_: torch.nn.ReLU, inplace: true}
- {_convert_: all, _target_: torch.nn.MaxPool2d, kernel_size: 2, stride: 2}
_convert_: all
_target_: torch.nn.Sequential
_convert_: all
_target_: torch.nn.Sequential
- - head
- _args_:
- {_convert_: all, _target_: torch.nn.AdaptiveAvgPool2d, output_size: 1}
- {_convert_: all, _target_: torch.nn.Flatten}
- {_convert_: all, _target_: torch.nn.Linear, in_features: '${hps.base_channels}',
out_features: '${hps.num_classes}'}
_convert_: all
_target_: torch.nn.Sequential
_convert_: all
_laco_: 1
_target_: torch.nn.Sequential
# The hps namespace is also addressable as its own fragment
hps_cfg = laco.load("configs://examples/pipelines/mnist_train.py#hps")
print("=== hps ===")
print(laco.dump(hps_cfg))
=== hps ===
{_laco_: 1, base_channels: 32, batch_size: 64, data_root: ./data, in_channels: 1,
learning_rate: 0.001, num_classes: 10, num_stages: 3, num_workers: 0}
The @L.task decorator (preview)
The mnist_train.py file also defines:
@L.task
def task(
model: nn.Module,
optimizer: optim.Optimizer,
loss: nn.Module,
loader: DataLoader,
num_steps: int = 1,
) -> None:
...
@L.task is a decorator that turns the function into a callable that accepts a
single DictConfig (the train bundle) and instantiates the arguments by name before
calling the underlying function. This is covered in 09.tasks-and-app-loop.ipynb; for
now, note that it is what powers python -m laco.examples.pipelines.mnist_train num_steps=2.
Section 5: Override Grammar Deep-Dive
Overrides follow the Hydra dotpath syntax: key.subkey=value. Laco threads them
through to OmegaConf merge semantics.
# Basic scalar override — change batch_size
cfg_bs128 = laco.load(
"configs://examples/pipelines/mnist_train.py#hps",
"hps.batch_size=128",
)
print(f"batch_size: {cfg_bs128.batch_size}") # noqa: LACO001
batch_size: 128
# Float override — scientific notation is supported
cfg_lr = laco.load(
"configs://examples/pipelines/mnist_train.py#hps",
"hps.learning_rate=5e-4",
)
print(f"learning_rate: {cfg_lr.learning_rate}") # noqa: LACO001
learning_rate: 0.0005
# Multiple overrides in a single call
cfg_multi = laco.load(
"configs://examples/pipelines/mnist_train.py#hps",
"hps.batch_size=256",
"hps.num_stages=2",
"hps.base_channels=16",
)
print(f"batch_size={cfg_multi.batch_size} "
f"num_stages={cfg_multi.num_stages} "
f"base_channels={cfg_multi.base_channels}") # noqa: LACO001
batch_size=256 num_stages=2 base_channels=16
# Show what changed between base and overridden configs
base_hps = laco.load("configs://examples/pipelines/mnist_train.py#hps")
ovrd_hps = laco.load(
"configs://examples/pipelines/mnist_train.py#hps",
"hps.batch_size=128",
"hps.learning_rate=5e-4",
)
for key in ("batch_size", "learning_rate", "num_stages"):
before = OmegaConf.select(base_hps, key)
after = OmegaConf.select(ovrd_hps, key)
changed = "<-- changed" if before != after else ""
print(f" {key:<18} {before!r:>12} → {after!r:<12} {changed}")
batch_size 64 → 128 <-- changed
learning_rate 0.001 → 0.0005 <-- changed
num_stages 3 → 3
URL-style overrides
Laco also accepts overrides embedded in the URL string using ?key=value syntax,
which is convenient when the full config path is stored in a single variable:
# URL-style: ?key=value before the # fragment separator
cfg_url = laco.load(
"configs://examples/pipelines/mnist_train.py"
"?hps.batch_size=32&hps.learning_rate=2e-3"
"#hps"
)
print(f"batch_size={cfg_url.batch_size} learning_rate={cfg_url.learning_rate}") # noqa: LACO001
batch_size=32 learning_rate=0.002
Override grammar summary
| Syntax | Meaning |
|---|---|
key=value | Set a scalar value |
key.subkey=value | Set a nested scalar |
+key=value | Append a new key (Hydra + prefix) |
~key | Remove a defaults-list entry (Hydra ~ prefix) |
?key=value | URL-style inline override (before #) |
?key=v1&key2=v2 | Multiple URL-style overrides |
Section 6: Config Composition
How the two source files relate, and how the pipeline bundle is assembled at compose time.
cnn_classifier.py declares @L.params class hps and make_cnn_classifier(**kw),
which builds L.call(nn.Sequential, root=True)(stem, stages, head) and exposes it as
model = make_cnn_classifier(...).
mnist_train.py imports make_cnn_classifier from cnn_classifier.py and declares:
| Name | Built with |
|---|---|
hps | @L.params |
model | make_cnn_classifier(...), threaded with hps.* references |
optimizer | L.partial(Adam)(...) |
loss | L.call(CrossEntropyLoss)() |
dataset | L.call(MNIST)(...) |
loader | L.call(DataLoader)(...) |
All five are collected into train = L.Dict(model=model, optimizer=optimizer, loss=loss, loader=loader).
Loading "...mnist_train.py#train" returns a single DictConfig with keys model,
optimizer, loss, loader — the whole pipeline in one fragment.
Section 7: __file__-Loading and Runnable Pipeline Modules
A config file that defines a @L.task entry point can be run directly as a module:
python -m laco.examples.pipelines.mnist_train num_steps=2
The canonical pattern for making a config file self-runnable is to use Python's
__file__ sentinel:
# From the bottom of mnist_train.py:
#
# if __name__ == "__main__":
# import laco
# cfg = laco.load(__file__ + "#train")
# task(cfg)
#
# Why __file__ instead of "configs://examples/pipelines/mnist_train.py"?
#
# __file__ is the absolute path on disk. laco.load() accepts both:
# - a "configs://" URL (resolved against the package search path)
# - a raw filesystem path (resolved directly)
# Using __file__ makes the script relocatable — it works even if the module
# is installed in a virtualenv, editable-installed, or symlinked.
import laco.examples.pipelines.mnist_train as _mt_module
print("Module file:", _mt_module.__file__)
Module file: /nix/store/1178ymd7883vh4fm70bqqn570ag2ikdy-laco-env/lib/python3.13/site-packages/laco/examples/pipelines/mnist_train.py
# Load via filesystem path (same as __file__ + "#train" from inside the module)
import importlib.resources, pathlib
module_path = pathlib.Path(_mt_module.__file__)
cfg_via_file = laco.load(str(module_path) + "#hps")
print("Loaded via __file__ path — batch_size:", cfg_via_file.batch_size) # noqa: LACO001
Loaded via __file__ path — batch_size: 64
configs:// URL vs filesystem path
| Form | Example | When to use |
|---|---|---|
configs:// URL | configs://examples/pipelines/mnist_train.py | References into the installed laco package's examples; stable across installs |
| Filesystem path | str(__file__) + "#fragment" | Self-loading from within the config file itself; works even outside the package |
| Relative path | "./my_config.py" | Local project configs not installed in any package |
The configs:// scheme is the recommended form for referencing library examples.
The __file__ form is the recommended form for self-running modules.
Section 8: Instantiating a Full Pipeline Bundle
With all pieces in place, here is how a training script consumes the pipeline config:
# Load the train bundle
train_cfg = laco.load(
"configs://examples/pipelines/mnist_train.py#train",
"hps.base_channels=16", # smaller model for faster instantiation in this notebook
"hps.num_stages=2",
)
# Instantiate each component individually
model_obj = laco.instantiate(train_cfg.model) # type: ignore[union-attr] # noqa: LACO001
optimizer_fn = laco.instantiate(train_cfg.optimizer) # type: ignore[union-attr] # noqa: LACO001
loss_obj = laco.instantiate(train_cfg.loss) # type: ignore[union-attr] # noqa: LACO001
# optimizer is a partial — call it with model.parameters()
opt = optimizer_fn(model_obj.parameters())
print("model :", type(model_obj).__name__)
print("optimizer :", type(opt).__name__)
print("loss :", type(loss_obj).__name__)
# Verify model works with a dummy forward pass
import torch
dummy = torch.randn(2, 1, 28, 28) # batch=2, channels=1, 28x28
out = model_obj(dummy)
print("output shape:", out.shape) # should be [2, 10]
model : Sequential
optimizer : Adam
loss : CrossEntropyLoss
output shape: torch.Size([2, 10])
Section 9: Nested Instantiation, DataLoader with Nested MNIST
Recursive instantiation is inherited from Hydra: when a config node contains a nested
node with a _target_, laco.instantiate will build the inner object first and pass
it to the outer constructor automatically.
# Inspect the loader config — it contains a nested dataset node
loader_cfg = laco.load("configs://examples/pipelines/mnist_train.py#loader")
print("=== loader config ===")
print(laco.dump(loader_cfg))
=== loader config ===
_convert_: all
_laco_: 1
_target_: torch.utils.data.DataLoader
batch_size: ${hps.batch_size}
dataset:
_convert_: all
_target_: torchvision.datasets.MNIST
download: true
root: ${hps.data_root}
train: true
transform:
_args_:
- _convert_: all
_target_: laco.language.List.target
items:
- {_convert_: all, _target_: torchvision.transforms.ToTensor}
- _convert_: all
_target_: torchvision.transforms.Normalize
mean:
_convert_: all
_target_: laco.language.List.target
items: [0.1307]
std:
_convert_: all
_target_: laco.language.List.target
items: [0.3081]
_convert_: all
_target_: torchvision.transforms.Compose
num_workers: ${hps.num_workers}
shuffle: true
# The nesting depth: loader → dataset → transform → [ToTensor, Normalize]
# laco.instantiate(loader_cfg) would:
# 1. Build Normalize(mean=[0.1307], std=[0.3081])
# 2. Build ToTensor()
# 3. Build Compose([ToTensor(), Normalize(...)])
# 4. Build MNIST(root=..., transform=Compose(...))
# 5. Build DataLoader(dataset=MNIST(...), batch_size=64, ...)
#
# This cell is *illustrative* — MNIST download would be triggered.
# Uncomment the line below in an environment with internet access:
# loader_obj = laco.instantiate(loader_cfg)
# print("loader:", loader_obj)
print("(Skipped — would download MNIST. Run in a connected environment to verify.)")
(Skipped — would download MNIST. Run in a connected environment to verify.)
Summary
| Concept | What it does |
|---|---|
L.Dict(k=v, ...) | Groups named config nodes into a single DictConfig root; accessed via #fragment |
L.List(n1, n2, ...) | An ordered DictConfig list of nodes; used for sequences like transform pipelines |
L.partial(T)(**kw) | A "partial constructor" node; instantiation returns functools.partial(T, **kw), which requires further arguments (e.g. model.parameters()) |
| Factory import pattern | A config file imports a factory function from another module and calls it with interpolation references to thread @L.params values through |
configs:// URL | Resolves against the installed package's config root; stable across environments |
__file__ loading | A config module loads itself by path; makes the module self-runnable as python -m ... |
| Recursive instantiation | laco.instantiate(cfg) builds nested objects bottom-up automatically |
Putting it all together:
- Use
@L.paramsto collect scalar hyperparameters in one place. - Use
L.call/L.partialto build lazy construction nodes for every component. - Thread
@L.paramsvalues through factory functions to keep the graph parametric. - Collect everything into
L.Dict(model=..., optimizer=..., ...)as thetrainbundle. - Load with
laco.load("...#train")and instantiate each component as needed.
Next: 09.tasks-and-app-loop.ipynb covers @L.task, the laco run CLI, and
how to structure a project so experiments are fully reproducible and launch-able from
the command line.