Nested runs for tuning

MLflow · Practical · 10 min

Sweeps produce dozens of runs; nesting keeps the experiment readable and the best trial easy to find.

A hyperparameter search is naturally a tree: one parent run for the search, one child run per configuration. Create children with nested=True inside an active parent:

with mlflow.start_run(run_name="search"):
    for params in grid:
        with mlflow.start_run(nested=True):
            ...

Children get the tag mlflow.parentRunId, and the UI shows them collapsed under the parent. Log the search configuration and the best result on the parent, so the parent alone answers "what did this sweep find?".

Patterns that work well:

  • Log each child's params and validation metric; log the best params and metric to the parent at the end.
  • Query children with filter_string=f"tags.mlflow.parentRunId = '{parent_id}'".
  • With tools like Optuna, start a nested run inside the objective function. In parallel workers, pass parent_run_id= explicitly to start_run instead of relying on the active run, because the active-run stack is per thread.

sklearn autolog does this automatically for GridSearchCV and RandomizedSearchCV, creating child runs for the top candidates (limited by max_tuning_runs).

Keep the number of logged metrics per child small; thousands of child runs with many step metrics make the UI slow.

Example

import mlflow

def train(lr, depth):                    # stand-in for real training
    return 0.9 - abs(lr - 0.1) - abs(depth - 4) * 0.01

mlflow.set_experiment("tuning-demo")
with mlflow.start_run(run_name="grid-search") as parent:
    best = None
    for lr in [0.01, 0.1, 0.3]:
        for depth in [2, 4]:
            with mlflow.start_run(nested=True):
                score = train(lr, depth)
                mlflow.log_params({"lr": lr, "depth": depth})
                mlflow.log_metric("val_score", score)
            if best is None or score > best[0]:
                best = (score, lr, depth)
    mlflow.log_metric("best_val_score", best[0])
    mlflow.log_params({"best_lr": best[1], "best_depth": best[2]})

kids = mlflow.search_runs(filter_string=f"tags.mlflow.parentRunId = '{parent.info.run_id}'")
print(len(kids), best)                   # 6 (0.9, 0.1, 4)

Remember

  • Use a parent run per search and nested=True child runs per trial
  • Summarize the best params and score on the parent
  • Children carry the mlflow.parentRunId tag for querying

Watch out for

  • Starting child runs from worker threads without passing parent_run_id, producing orphaned top-level runs.

Check yourself

  1. How can you list all trials of a sweep whose parent run ID is p?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: mlflow.org/docs/latest/ml/traditional-ml/tutorials/hyperparameter-tuning/part1-child-runs/

Builds on

Unlocks

Nothing builds on this yet.