Foundation Examples
Foundation Examples
Tier 0–1: four self-contained configs that cover the core laco primitives:
L.call,L.partial,@L.params,L.repeat,L.OrderedDict, andL.required.
All source files live under sources/laco/examples/ and are exercised by tests/test_examples.py.
1. linear_regression.py: Simplest laco config
The smallest realistic laco configuration: a single nn.Linear with a partially-applied SGD optimizer. Everything in laco starts here.
Full source
import laco.language as L
from torch import nn, optim
@L.params
class hps:
in_features: int = 8
out_features: int = 1
bias: bool = True
learning_rate: float = 1e-2
momentum: float = 0.9
model = L.call(nn.Linear, root=True)(
in_features=hps.in_features,
out_features=hps.out_features,
bias=hps.bias,
)
optimizer = L.partial(optim.SGD)(
lr=hps.learning_rate,
momentum=hps.momentum,
)
Annotated walkthrough
@L.params class hps
Declares a flat hyperparameter namespace. Each annotated attribute becomes an interpolation node in the config tree: hps.in_features resolves to ${hps.in_features} at load time.
L.call(nn.Linear, root=True)L.call(T) produces a config node that will instantiate T when laco.instantiate is called. The root=True flag marks this node as the top-level instantiation target, so laco.load("...#model") returns it directly. Without root=True, the node is a nested sub-config.
L.partial(optim.SGD)
Like L.call, but instantiation returns a partial (a callable that still needs model.parameters()). This is the correct pattern for optimizers because parameter tensors do not exist until the model is built.
Load and instantiate
import laco
# Load the model config node directly via fragment selector
cfg = laco.load("configs://examples/linear_regression.py#model")
model = laco.instantiate(cfg)
# Load the full module (model + optimizer + hps)
full = laco.load("configs://examples/linear_regression.py")
model = laco.instantiate(full.model)
opt_factory = laco.instantiate(full.optimizer) # returns partial
opt = opt_factory(model.parameters())
Override demo
# Inline query-string overrides (applied before instantiation)
cfg = laco.load(
"configs://examples/linear_regression.py?hps.in_features=16&hps.out_features=4#model"
)
# Positional override arguments (Hydra-style)
cfg = laco.load(
"configs://examples/linear_regression.py",
"hps.in_features=16",
"hps.out_features=4",
)
What this demonstrates
@L.params: flat hyperparameter namespace with typed defaultsL.call(T, root=True): eager construction target with fragment-select supportL.partial(T): deferred factory for objects that need runtime arguments (optimizers, schedulers)- Fragment selector (
#model) for loading a specific sub-tree
@L.params is the right tool for scalar hyperparameters like these. When a field
needs to be one of several swappable alternatives (e.g. choosing among optimizer or
activation variants), see Typed-Group Variants for L.Group.
2. mlp.py: Nested composition with L.repeat
Source:
sources/laco/examples/mlp.py
Smallest non-trivial laco config: stacks Linear → activation blocks using L.repeat and groups named sections with L.OrderedDict. The make_mlp factory function is the first example of a config factory: a plain Python function that returns a config node, not a model.
Full source
import laco.language as L
from torch import nn
@L.params
class hps:
dim_in: int = 128
dim_out: int = 128
dim_hidden: int = 256
num_layers: int = 3
activation: type[nn.Module] = nn.ReLU
def make_mlp(*, dim_in, dim_out, dim_hidden, num_layers, activation):
return L.call(nn.Sequential, root=True)(
L.OrderedDict(
(
"input",
L.call(nn.Sequential)(
L.call(nn.Linear)(
in_features=dim_in,
out_features=dim_hidden,
),
L.call(activation)(),
),
),
(
"hidden",
L.call(nn.Sequential, expand_args=True)(
L.repeat(
num_layers,
L.call(nn.Sequential)(
L.call(nn.Linear)(
in_features=dim_hidden,
out_features=dim_hidden,
),
L.call(activation)(),
),
),
),
),
(
"output",
L.call(nn.Sequential)(
L.call(nn.Linear)(
in_features=dim_hidden,
out_features=dim_out,
)
),
),
)
)
model = make_mlp(
dim_in=hps.dim_in,
dim_out=hps.dim_out,
dim_hidden=hps.dim_hidden,
num_layers=hps.num_layers,
activation=hps.activation,
)
Annotated walkthrough
make_mlp(...): config factory function
A plain Python function that constructs and returns a config tree. Calling it does not instantiate any PyTorch modules; it builds a declarative description. This pattern enables reuse: other pipelines can import make_mlp and embed its result in a larger config.
L.OrderedDict(("name", node), ...)
Creates a named-section mapping inside nn.Sequential. Each pair becomes a key-value entry in the config; instantiation preserves insertion order. Use this instead of positional args when sections need stable names for inspection or partial override.
L.repeat(num_layers, block)
Expands block into a list of num_layers identical config nodes at config-build time. The expansion happens before instantiation, so each repeated entry is a separate node that can be overridden independently.
L.call(nn.Sequential, expand_args=True)(...)
The expand_args=True flag tells laco to unpack a list argument into positional parameters when calling nn.Sequential. This is required for the hidden-layer stack because nn.Sequential accepts *modules, not a list.
Equivalent config tree (abridged YAML)
_target_: torch.nn.Sequential
_args_:
input:
_target_: torch.nn.Sequential
_args_:
- {_target_: torch.nn.Linear, in_features: 128, out_features: 256}
- {_target_: torch.nn.ReLU}
hidden:
_target_: torch.nn.Sequential # expand_args unpacks the list below
_args_:
- # layer 0
_target_: torch.nn.Sequential
_args_:
- {_target_: torch.nn.Linear, in_features: 256, out_features: 256}
- {_target_: torch.nn.ReLU}
- # layer 1 … layer N-1 (identical copies)
output:
_target_: torch.nn.Sequential
_args_:
- {_target_: torch.nn.Linear, in_features: 256, out_features: 128}
Load and instantiate
import laco
cfg = laco.load("configs://examples/mlp.py#model")
model = laco.instantiate(cfg)
Override demo
# Wider hidden layer, more layers
cfg = laco.load(
"configs://examples/mlp.py",
"hps.dim_hidden=512",
"hps.num_layers=6",
)
model = laco.instantiate(cfg.model)
# Swap activation type
cfg = laco.load(
"configs://examples/mlp.py",
"hps.activation=torch.nn.GELU",
)
What this demonstrates
- Config factory functions (
make_mlp): plain Python, returns config not model L.repeat(n, block): homogeneous layer stacks declared at config timeL.OrderedDict: named sections insidenn.Sequentialexpand_args=True: unpacking a repeated list into positional*args
3. cnn_classifier.py: L.repeat for stacked conv stages
A CNN image classifier with a stem, repeated pooling stages, and a global-average-pool head. Introduces _conv_block as a private config helper and make_cnn_classifier as the importable factory.
Full source
import laco.language as L
from torch import nn
@L.params
class hps:
in_channels: int = 3
base_channels: int = 32
num_stages: int = 3
num_classes: int = 10
def _conv_block(in_ch, out_ch, *, pool: bool):
layers = [
L.call(nn.Conv2d)(
in_channels=in_ch,
out_channels=out_ch,
kernel_size=3,
padding=1,
bias=False,
),
L.call(nn.BatchNorm2d)(num_features=out_ch),
L.call(nn.ReLU)(inplace=True),
]
if pool:
layers.append(L.call(nn.MaxPool2d)(kernel_size=2, stride=2))
return L.call(nn.Sequential)(*layers)
def make_cnn_classifier(*, in_channels, base_channels, num_stages, num_classes):
return L.call(nn.Sequential, root=True)(
L.OrderedDict(
("stem", _conv_block(in_channels, base_channels, pool=False)),
(
"stages",
L.call(nn.Sequential, expand_args=True)(
L.repeat(
num_stages,
_conv_block(base_channels, base_channels, pool=True),
),
),
),
(
"head",
L.call(nn.Sequential)(
L.call(nn.AdaptiveAvgPool2d)(output_size=1),
L.call(nn.Flatten)(),
L.call(nn.Linear)(
in_features=base_channels,
out_features=num_classes,
),
),
),
)
)
model = make_cnn_classifier(
in_channels=hps.in_channels,
base_channels=hps.base_channels,
num_stages=hps.num_stages,
num_classes=hps.num_classes,
)
Annotated walkthrough
_conv_block(in_ch, out_ch, *, pool): private config helper
Returns a L.call(nn.Sequential)(...) config node, not a live module. The conditional if pool: adds MaxPool2d to the layer list before the config is assembled. Python control flow is fully available at config-build time.
Stem vs. stages
The stem uses pool=False (no spatial reduction) while each stage uses pool=True (halves spatial size). Both stem and stages are assembled as config nodes in L.OrderedDict: the Python flag affects the config structure, not runtime behavior.
Head with L.call(nn.AdaptiveAvgPool2d) and L.call(nn.Flatten)
Arbitrary PyTorch modules can appear as config targets: laco does not require wrapper classes or special registration. output_size=1 is passed as a keyword argument and stored verbatim in the config node.
make_cnn_classifier is importable
The pipeline example (pipelines/mnist_train.py) imports and calls this function directly:
from laco.examples.cnn_classifier import make_cnn_classifier
model = make_cnn_classifier(in_channels=hps.in_channels, ...)
Load and instantiate
import laco
cfg = laco.load("configs://examples/cnn_classifier.py#model")
model = laco.instantiate(cfg) # nn.Sequential, instantiation happens here
Override demo
# Grayscale input, more stages
cfg = laco.load(
"configs://examples/cnn_classifier.py",
"hps.in_channels=1",
"hps.num_stages=4",
"hps.base_channels=64",
)
model = laco.instantiate(cfg.model)
What this demonstrates
- Private config helpers (
_conv_block): plain functions that return config nodes - Conditional config construction using Python
ifat config-build time L.repeatfor homogeneous pooling stages- Head construction with
AdaptiveAvgPool2d+Flatten+Linearinline - Importable factory pattern: the same function used standalone and as a pipeline component
4. text_classifier.py: L.required[T]() for mandatory fields
Bag-of-embeddings text classifier (Embedding → mean-pool → Linear). Introduces L.required[T](): a sentinel that marks a field as mandatory and raises a clear error if the caller forgets to supply it.
Full source
import laco.language as L
from laco.examples.layers.mean_pool import MeanPool
from torch import nn
@L.params
class hps:
vocab_size: int = L.required[int]()
embed_dim: int = 64
num_classes: int = 2
padding_idx: int | None = 0
model = L.call(nn.Sequential, root=True)(
L.OrderedDict(
(
"embed",
L.call(nn.Embedding)(
num_embeddings=hps.vocab_size,
embedding_dim=hps.embed_dim,
padding_idx=hps.padding_idx,
),
),
("pool", L.call(MeanPool)()),
(
"head",
L.call(nn.Linear)(
in_features=hps.embed_dim,
out_features=hps.num_classes,
),
),
)
)
Annotated walkthrough
vocab_size: int = L.required[int]()L.required[T]() is a typed sentinel value. The type parameter [int] is for static analysis (pyright/mypy see vocab_size: int). At runtime, laco detects this sentinel during resolution and raises MissingMandatoryValue with a precise path if the value has not been supplied by the caller.
L.OrderedDict pipeline
The three stages (embed, pool, head) are declared as named entries. MeanPool is a custom nn.Module from laco.examples.layers.mean_pool; it is treated identically to any PyTorch built-in.
Propagation of required fieldshps.vocab_size is passed directly into L.call(nn.Embedding)(num_embeddings=hps.vocab_size, ...). The required sentinel propagates through the config tree; laco resolves it only at laco.instantiate time, not at laco.load time.
Load with the required field supplied
import laco
# Via query-string
cfg = laco.load(
"configs://examples/text_classifier.py?hps.vocab_size=1000#model"
)
model = laco.instantiate(cfg)
# Via positional override
cfg = laco.load(
"configs://examples/text_classifier.py",
"hps.vocab_size=1000",
)
model = laco.instantiate(cfg.model)
What happens without the required field
cfg = laco.load("configs://examples/text_classifier.py#model")
laco.instantiate(cfg)
# raises: omegaconf.errors.MissingMandatoryValue:
# Missing mandatory value: hps.vocab_size
Override demo
cfg = laco.load(
"configs://examples/text_classifier.py",
"hps.vocab_size=30000",
"hps.embed_dim=128",
"hps.num_classes=5",
)
model = laco.instantiate(cfg.model)
What this demonstrates
L.required[T](): typed mandatory sentinel, raisesMissingMandatoryValueif unset- Named pipeline with
L.OrderedDict(embed → pool → head) - Custom module (
MeanPool) as a config target: no special registration needed - Caller-supplied required fields via query-string or positional overrides