EXAMPLES

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, and L.required.

All source files live under sources/laco/examples/ and are exercised by tests/test_examples.py.


1. linear_regression.py: Simplest laco config

Source: sources/laco/examples/linear_regression.py

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 defaults
  • L.call(T, root=True): eager construction target with fragment-select support
  • L.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 time
  • L.OrderedDict: named sections inside nn.Sequential
  • expand_args=True: unpacking a repeated list into positional *args

3. cnn_classifier.py: L.repeat for stacked conv stages

Source: sources/laco/examples/cnn_classifier.py

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 if at config-build time
  • L.repeat for homogeneous pooling stages
  • Head construction with AdaptiveAvgPool2d + Flatten + Linear inline
  • Importable factory pattern: the same function used standalone and as a pipeline component

4. text_classifier.py: L.required[T]() for mandatory fields

Source: sources/laco/examples/text_classifier.py

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, raises MissingMandatoryValue if 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