Config Management

Config management controls live hyperparameters, experiment identity, and runtime paths during training.

Hyperparameter wrapper parameters

wl.watch_or_edit(..., flag="hyperparameters", ...) key parameters:

Parameter

Default

Behavior

defaults

None

Values registered before the YAML is first read. They seed the in-memory config only — the watcher reads the file, it never writes it, so write the YAML yourself if you want it editable from the start.

poll_interval

1.0

Reload period (seconds) for file-based config updates.

checkpoint_manager

None

Checkpoint load/save behavior override for config state.

Registration patterns

Dict-based:

import weightslab as wl

hp = wl.watch_or_edit(
    {
        "experiment_name": "exp_a",
        "root_log_dir": "./logs/exp_a",
        "optimizer": {"lr": 1e-3},
        "data": {"train_loader": {"batch_size": 16}},
    },
    flag="hyperparameters",
)

YAML-based with polling:

hp = wl.watch_or_edit(
    "./config.yaml",
    flag="hyperparameters",
    defaults={"optimizer": {"lr": 1e-3}},
    poll_interval=1.0,
)

watch_or_edit rebinds the caller’s variable to the returned proxy, so pass the path as a fresh string (str(config_path)) when you still need the path afterwards.

Runtime SDK operations

# Read
lr = hp["optimizer"]["lr"]

# Write (in-place)
hp["optimizer"]["lr"] = 5e-4
hp["data"]["train_loader"]["batch_size"] = 32

root_log_dir behavior

root_log_dir determines where experiment artifacts are stored:

  • checkpoints and version states

  • logger history

  • generated reports

  • notebook artifacts

Example:

experiment_name: classifier_v1
root_log_dir: ./logs/classifier_v1

Standalone config-only integration (UI + CLI ready)

A complete, runnable script with nothing but the configuration registered: no model, no data, no signals. Its loop only reads the config each step and prints what changed, so you can watch a value propagate from any of the three places it can be edited — the YAML file, set_hp in the CLI, or the studio panel.

Bundled example: weightslab/examples/PyTorch/wl-standalone-config/main.py

weightslab start example --config    # writes config.yaml on first run
weightslab cli                       # attach a terminal, in another shell
weightslab start                     # open Weights Studio, in a third shell
def main(argv=None) -> int:
    args = parse_args(argv)

    config_path = Path(args.config).resolve()
    config_path.parent.mkdir(parents=True, exist_ok=True)
    if not config_path.exists():
        # `defaults=` seeds the in-memory config; the watcher only *reads* the
        # file, so write it once here to make it editable from the start.
        config_path.write_text(yaml.safe_dump(DEFAULT_CONFIG, sort_keys=False),
                               encoding="utf-8")
        print(f"[config-level] wrote {config_path}")

    import weightslab as wl

    # --- the only WeightsLab registration in this file ------------------------
    # str(...) is deliberate: watch_or_edit rebinds the caller's variable to the
    # returned proxy, and passing a fresh string keeps `config_path` intact.
    hp = wl.watch_or_edit(
        str(config_path),
        flag="hyperparameters",
        defaults=copy.deepcopy(DEFAULT_CONFIG),
        poll_interval=args.poll_interval,
    )
    # --------------------------------------------------------------------------

    wl.serve(serving_grpc=not args.no_grpc, serving_cli=not args.no_cli,
             grpc_port=args.grpc_port)
    print("=" * 70)
    print(" CONFIG-ONLY standalone — attach with `weightslab cli`, UI with `weightslab start`")
    print(f" config={config_path}")
    # Each reload makes the FILE authoritative, so a root_log_dir that was only
    # injected at registration does not survive it; ask for the directory in use.
    print(f" experiment dir={experiment_dir()}")
    print(" try: set_hp optimizer.lr 0.0005   (or edit the YAML)")
    print("=" * 70)
    wl.start_training(timeout=3)

    total = args.steps or int(read_path(hp, "training_steps_to_do", 300) or 300)
    last = {key: read_path(hp, key) for key in WATCHED}
    print(f"[config-level] step 0: " + ", ".join(f"{k}={v}" for k, v in last.items()))

    for step in range(1, total + 1):
        # A real loop would use these values (lr on the optimizer, batch size on
        # the loader). Here we only observe them, so the config level stands alone.
        current = {key: read_path(hp, key) for key in WATCHED}
        changed = {k: v for k, v in current.items() if v != last[k]}
        if changed:
            print(f"[config-level] step {step}: changed -> "
                  + ", ".join(f"{k}: {last[k]} -> {v}" for k, v in changed.items()))
            last = current
        if not current.get("is_training", True):
            print(f"[config-level] step {step}: is_training=False — idling")
        time.sleep(args.step_delay)

    print(f"[config-level] loop finished after {total} steps; final config:")
    print(yaml.safe_dump({k: last[k] for k in WATCHED}, sort_keys=False).strip())

    wl.keep_serving(timeout=args.serve_timeout)
    return 0

Then, from the attached CLI:

hp                                     # -> ['main']
hp main                                # the whole config
set_hp optimizer.lr 0.0005             # the loop prints the change
set_hp data.train_loader.batch_size 64

Note

The file watcher is one-way: it loads the YAML when its mtime changes, and set_hp / studio edits change the live config without writing the file back. Saving the YAML after an in-memory edit therefore reinstates the file’s values.

CLI and UI surfaces

CLI:

  • hp / hp <name>

  • set_hp [hp_name] <key.path> <value>

  • status for current registered configuration

UI:

  • Hyperparameters panel runtime edits

  • Agent-driven config changes (for example “set batch size to 32”)