HOW-TO

Publishing a reproducible app with laco app

Publishing a reproducible app with laco app

laco app lets project authors expose a simple argparse-style CLI for their configs so that collaborators and external users can reproduce or extend experiments without needing to understand Hydra.

How it works

  1. The author annotates their config class with @L.params and marks individual hyperparameters with L.param() / L.Param[T] / L.Hidden[T].
  2. The project's pyproject.toml registers a laco.apps entry point pointing to the entry function.
  3. Users run laco app <dist> <entry> --flag value: laco translates the flags into Hydra overrides and runs the task.

Author setup

1. Declare hyperparameters with @L.params

# configs/train.py
import laco.language as L

@L.params          # default: prefix="", so flags are flat (--lr, --epochs)
class hps:
    """Hyperparameters.

    Parameters
    ----------
    lr : float
        Learning rate.
    epochs : int
        Number of training epochs.
    """
    lr: float = 0.001
    epochs: int = 100

    # Hidden from the CLI but still in the config:
    _internal_state: L.Hidden[str] = "init"

seed = L.param(42, help="Global random seed")

2. Write the entry point

# my_project/train.py
import laco
import laco.language as L

@laco.main(config_name="train", config_path="configs")
@L.task
def run(hps, seed: int):
    print(f"Training with lr={hps.lr}, seed={seed}")
    ...

if __name__ == "__main__":
    run()

3. Register the entry point in pyproject.toml

[project.entry-points."laco.apps"]
train = "my_project.train:run"

After pip install -e . (or any install that registers the entry points), users can invoke:

laco app my-project train --lr 0.01 --epochs 200 --seed 7

User experience

Help

laco app my-project train --help
usage: laco app my-project train [--lr LR] [--epochs EPOCHS] [--seed SEED]
                                  [--dry-run] [--save-dir DIR]

Run my-project:train with a simple flag-based interface.

options:
  --lr LR          Learning rate. (default: 0.001)
  --epochs EPOCHS  Number of training epochs. (default: 100)
  --seed SEED      Global random seed (default: 42)
  --dry-run        Print the resolved config as YAML and exit without running.
  --save-dir DIR   Save resolved config.yaml into DIR before running.

Running with overrides

# Override a single value
laco app my-project train --lr 1e-4

# Override several values
laco app my-project train --lr 1e-4 --epochs 500 --seed 0

# Inspect the resolved config without running
laco app my-project train --lr 1e-4 --dry-run

# Save the resolved config to disk
laco app my-project train --lr 1e-4 --save-dir ./outputs/run-001

Fuzzy suggestions on typos

If you mistype a flag, laco app shows the closest match:

laco app my-project train --learing-rate 0.01
# error: argument --learing-rate: unrecognized arguments
# Unknown flag '--learing-rate'. Did you mean: --lr?

Flag prefix rules

The prefix argument to @L.params controls how flags are named:

prefix=Config keyCLI flag
"" (default)hps.lr--lr
Nonehps.lr--hps-lr
"model"hps.lr--model-lr

Underscores in field names and prefixes are converted to hyphens in flags.

Hiding parameters

Use L.Hidden[T] as the annotation, or pass hidden=[...] to @L.params, to keep fields in the config without exposing them as CLI flags:

@L.params(hidden=["weight_decay"])
class hps:
    lr: float = 0.001
    weight_decay: float = 1e-4   # not exposed as --weight-decay

Or use the type annotation directly:

@L.params
class hps:
    lr: float = 0.001
    weight_decay: L.Hidden[float] = 1e-4

To hide an entire @L.params block from laco app (for example, a block used only internally), pass exclude=True:

@L.params(exclude=True)
class internal:
    debug: bool = False

One-off parameters with L.param()

For values that don't belong in a @L.params class, use L.param() at module level:

seed = L.param(42, help="Random seed", validator=L.Range(0, 2**31))
opt  = L.param("adam", choices=["adam", "sgd", "adamw"],
               help="Optimizer name")

These appear as --seed and --opt in the CLI alongside the @L.params flags.

Validators

import laco.language as L

lr = L.param(1e-3, validator=L.Range(1e-7, 1.0))
opt = L.param("adam", validator=L.Choices("adam", "sgd", "adamw"))
ValidatorEffect
L.Range(min, max)Value must be in [min, max]
L.Choices(*opts)Value must be one of opts
Any callableCalled with the value; raise ValueError to reject

Boolean flags

Boolean parameters get --flag / --no-flag pairs automatically:

@L.params
class hps:
    use_amp: bool = True
laco app my-project train --no-use-amp