> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuneplane.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Write the experiment config

> defaults inheritance, _override_, interpolation, and which layer wins at submit time.

```yaml experiments/my-grpo/config.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
defaults:
  - ../../configs/base/grpo_math_1B.yaml
  - ../../configs/models/qwen3.5-9b.yaml

policy:
  optimizer:
    kwargs:
      lr: 2.0e-6

grpo:
  num_generations_per_prompt: 8

loss_fn:
  reference_policy_kl_penalty: 0.01
```

Inherit a base, then write only what you changed. `tp validate` and `tp submit` resolve this
file with the same code the cluster-side launcher uses.

A custom job can ignore this file entirely, if its `train.sh` never reads it. Catalogue methods
always read it.

## How defaults resolve

`defaults` entries are paths relative to **this** file. Each one is resolved recursively, so a
base may have `defaults` of its own. Later entries win over earlier ones, and keys in the
experiment file win last.

The comment block at the top of the scaffold lists the keys people change most, with ranges
taken from the recipe. It is a cheat sheet, not configuration.

## Which layer wins

Four layers, later wins:

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
the recipe's template
└─ experiments/<name>/config.yaml
   └─ the hardware profile from the server registry (--profile)
      └─ tp submit --set dotted.key=value
```

Topology comes from the profile, not from you: the adapter overwrites node and GPU counts from
`TUNEPLANE_CLUSTER_NUM_NODES` and `TUNEPLANE_CLUSTER_GPUS_PER_NODE`. Going from four GPUs to
eight is a different `--profile`, never a config edit.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp submit my-grpo --profile h200:8 \
  -s policy.optimizer.kwargs.lr=1e-6 \
  -s loss_fn.reference_policy_kl_penalty=0.05
```

`--set` is type-checked against the recipe's declared parameters before anything is uploaded. A
typo fails on your machine.

## Replace a whole mapping instead of merging it

Deep merge is the default. Put `_override_: true` in a mapping to replace it wholesale. The
marker is stripped and never reaches the trainer.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
policy:
  generation:
    _override_: true
    temperature: 1.0
    top_p: 0.95
```

## Interpolation

`${...}` values stay as strings, and `tp validate` skips numeric checks on them — it cannot see
the value. Two forms are understood when checking that a local data path exists,
`${oc.env:GSM8K_DATA_DIR}/train.jsonl` and `${GSM8K_DATA_DIR}/train.jsonl`, for the names
`GSM8K_DATA_DIR`, `ALPACA_DATA_DIR` and `QA_RL_DATA_DIR`. Anything else containing `${` is left
alone.

## Unknown keys are errors

The config is validated in struct mode. A misspelled key fails before the job occupies a node,
which is the point. Do not add trainer-only keys the recipe did not declare.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp methods nemo-rl/grpo    # every declared key: type, default, range, description
```

## Batch-size relationships

Recipes that declare a relationship between rollout batch and train batch check it. Not
divisible is an error. Divisible but unequal gives an off-policy warning, which is sometimes
what you want.

## Check it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp validate my-grpo
```

No output means the config resolves and every key is declared and in range.

## Next

[Submitting a job](https://docs.tuneplane.ai/en/guides/submit.md) · [Experiments](https://docs.tuneplane.ai/en/guides/experiments.md) ·
[Method catalogue](https://docs.tuneplane.ai/en/guides/methods.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.