Lego-RL

Configuration Reference

The three config layers, every variable a run reads, and what it writes out

The values a run must have are in Configuration. This page lists everything else you can set, and what each value changes.

There is no config.yaml. A run is one .env config file passed to one of three runners:

bash scripts/train/train.sh  scripts/train/configs/my_run.env
bash scripts/eval/eval.sh    scripts/eval/configs/my_eval.env
bash scripts/infer/infer.sh  scripts/infer/configs/my_infer.env

The runner sources the config, applies the model preset, resolves the scaffold and backend, runs preflight, then execs verl (fully_async_main or main_ppo) with the Hydra overrides built from those values. Every omitted key falls back to a default, so a config file is ~20 lines rather than ~100.

This page follows the order a config is actually written: pick a template (§ Axes), fill in what has no default (§ What every config sets), tune what you are experimenting with (§ What you tune per run), and set the cluster's own values once (§ Site variables).

Legacy scripts

scripts/sync_1node_cc.sh, scripts/fully_async_*.sh, scripts/eval_*.sh, and scripts/infer_*.sh still run. They spell out every value inline, so a fix in one does not reach the others. They also read different variable names than the runners do — see the names that look right but are not. Use the runners for new work; retire an old script once a runner config covers the same experiment.

Config layers

Every value belongs to exactly one layer:

LayerFileScopeChanges
Sitescripts/lib/site.envthis cluster: registry, nydus mirror, hostPath mounts, kubeconfig, docker daemon, MODEL_ROOT, NEW_VERL_DIRonce, when you move clusters
Templatescripts/{train,eval,infer}/templates/*.enva structural combination (mode × backend × scaffold × model)never: you copy it
Configscripts/{train,eval,infer}/configs/*.envone experiment: names, data, topology, cadenceevery run

No cluster-specific value is hardcoded in the runners or libs. With nothing set, the stack falls back to a portable path: in-pod inline build, no mounts, no image rewrite. That path is slower but runs anywhere.

The site layer is opt-in

lib/common_env.sh sources site.env, but the three runners do not source common_env.sh — only scripts/setup_env.sh and the older single-file scripts do. So a value written only in site.env never reaches train.sh / eval.sh / infer.sh: the configs under scripts/train/configs/ carry K8S_KUBECONFIG and HARBOR_OPENSWE_IMAGE_REGISTRY themselves. To pull the site file in, source it as the first line of a config:

source "$REPO_ROOT/scripts/lib/site.env"

site.env holds your cluster's registry addresses, kubeconfig path and mounts — do not share it. Copy scripts/lib/site.example.env when onboarding a new cluster.

Axes

Only these four require a different template. Everything else is a parameter you set in the config.

AxisValuesWhat it switches
TRAINING_MODEasync | syncfully_async_main + fully_async_fsdp.yaml (split train/rollout pools) vs main_ppo + sync.yaml (one colocated pool)
MODEL_ENGINEveomni | fsdpmodel_engine=veomni + veomni.* (required for hybrid-GDN models) vs strategy=fsdp2 + fsdp_config.*. Follows the model; see the table below
SCAFFOLDohsdk | oh | cc | ocagent class + runtime image + agent-loop config. ohsdk is primary; cc = Claude Code; oc = OpenCode
BACKENDk8s | dockersandbox backend; see Backends

Node counts (NNODES, N_NODES_TRAIN, N_NODES_ROLLOUT) are parameters, not axes: the same template scales from one node to four.

Per-model values

These values are correlated: pick a model and the rest follow. Set them explicitly in the config. There is no preset mechanism that fills them for you.

ModelMODEL_ENGINESP_SIZEWindow (prompt + response)TOOL_CALL_PARSERENABLE_R3
Qwen3.5-35B-A3B (hybrid-GDN MoE)veomni440 000 + 91 072qwen3_coderauto → True
Qwen3-30B-A3B-Instruct-2507 (MoE)veomni440 000 + 91 072by scaffold †auto → True
Qwen3.6-27B (dense hybrid-mamba)fsdp1167 232 + 32 768qwen3_coderauto → False

ENABLE_R3 is the one value you do not normally set: it defaults to whether MODEL_PATH is a MoE checkpoint, read from its config.json. Forcing it on for a dense model is a validation FATAL; see Run Validation.

tool_parser is not a function of the model alone. Qwen3.5/3.6 chat templates emit XML → always qwen3_coder. The 30B needs qwen3_coder under cc but hermes under ohsdk/oh. Validation checks the combination; a wrong value means a 100% tool-parse failure rate on step 0 (see Tool-call parser mismatch).

What every config sets

train: PROJECT_NAME · EXP_NAME · TRAIN_FILES · VAL_FILES · TOTAL_EPOCHS · topology.

VariableMeaning
PROJECT_NAME / EXP_NAMEwandb project + experiment name; also the checkpoints/ and harbor_trials/ path
TRAIN_FILES / VAL_FILESTask indexes: thin parquet rows pointing at Harbor task dirs
TOTAL_EPOCHSpasses over the task index; _template.env marks it CHANGEME
NNODES / N_NODES_TRAIN / N_NODES_ROLLOUTtopology; see below
POD_NAME_PREFIXlabel prefix for this run's sandbox pods (used by cleanup)

eval replaces the data inputs with DATASET_PATH (a directory of harbor task dirs) or DATASET_NAME + N_TASKS, and adds serving variables: GEN_TP, EVAL_TEMPERATURE, MAX_INPUT_TOKENS / MAX_OUTPUT_TOKENS / MAX_MODEL_LEN, N_CONCURRENT, and EVAL_ENABLE_EXPERT_PARALLEL (dense → false, MoE → true).

infer takes a base INDEX_FILE plus RESULTS_DIR + OUTPUT_INDEX, an optional per-node INSTANCES_FILE shard, N_TRIALS / N_CONCURRENT, and the vLLM topology (GEN_TP, GEN_DP, VLLM_NNODES, VLLM_HEAD_HOST). A multi-node DP run uses the same config on every node; the node whose IP matches VLLM_HEAD_HOST becomes rank 0.

Topology

NNODES must equal N_NODES_TRAIN + N_NODES_ROLLOUT. Validation is fatal otherwise, because a mismatched Ray cluster hangs in placement rather than failing.

NNODES=1                 # colocated: rollout shares the training node
N_NODES_TRAIN=1
N_NODES_ROLLOUT=0

NNODES=3                 # disaggregated: 2 training + 1 rollout
N_NODES_TRAIN=2
N_NODES_ROLLOUT=1

SP_SIZE (default 8) sets sequence parallelism; train_world / SP_SIZE is the data-parallel width and must be a whole number. Scaling Up covers when a second node actually helps — rollout replicas, not the concurrency gate, are usually what set validation wall-clock.

What you tune per run

One KEY=VALUE line per value, in the run config. Multi-word values must be quoted (EVAL_VLLM_EXTRA_ARGS="--enforce-eager --dtype bfloat16"). The values below are the template defaults (scripts/templates/verl/common.env) — write a line only to move off one.

Scale and cadence

TRAIN_BSZ=64                    # prompts per step
TRAIN_MINI_BSZ=64               # gradient-accumulation width
N_RESP=8                        # responses per prompt, the GRPO group size → 512 trials/step
USE_DYNAMIC_BSZ=True            # pack by token budget rather than sequence count

MAX_PROMPT=30000                # context window = prompt + response
MAX_RESP=170000                 # per-model windows are in the table above

TRAINER_SAVE_FREQ=10            # checkpoint cadence
TRAINER_TEST_FREQ=10            # validation cadence
TRAINER_VAL_BEFORE_TRAIN=True   # baseline before the first update

Algorithm and sampling

ADV_ESTIMATOR=grpo              # ppo | grpo
POLICY_LOSS_MODE=gspo           # sequence-level policy loss
ACTOR_LR=1e-6
LR_SCHEDULER=constant           # keep constant on fully-async (see Run Validation)
KL_LOSS_COEF=0.001

TEMPERATURE=1.0                 # rollout sampling
VAL_TEMPERATURE=0.7             # validation sampling
N_CONCURRENT=32                 # rollout parallelism

The default profile is therefore GRPO advantages with a GSPO policy loss, lr 1e-6, and 64 × 8 = 512 trials per step. The meaning of each symbol is defined in Core Concepts.

Names that look right but are not

Each name on the left is read by the legacy scripts/*.sh scripts, or is the Hydra key rather than the variable. Written in a runner config it is silently ignored: the run starts, with the template default instead of your value.

Written in a configWhat the runner reads
SAVE_FREQ / TEST_FREQ / VAL_BEFORE_TRAINTRAINER_SAVE_FREQ / TRAINER_TEST_FREQ / TRAINER_VAL_BEFORE_TRAIN
adv_estimatorADV_ESTIMATOR
policy_loss_modePOLICY_LOSS_MODE
learning_rateACTOR_LR

To confirm any variable took effect, run bash scripts/train/train.sh --structure-only <config> and read the schedule, batch and algorithm lines it prints.

Without editing a file

Any variable can be overridden for a single run from the command line:

K8S_KUBECONFIG=/tmp/other.yaml bash scripts/train/train.sh <config>

VENV_PATH reuses a prebuilt venv; verify its editable installs resolve to the trees you expect, see Venv. The wandb API key belongs in your shell, never in a config file:

export WANDB_API_KEY=...
# or: WANDB_MODE=disabled

Site variables

scripts/lib/site.env, set once per cluster:

K8S_KUBECONFIG=/path/to/kubeconfig.yaml        # which K8s cluster
HARBOR_OPENSWE_IMAGE_REGISTRY=reg:5001/openswe # prebuilt task images

The full surface. An empty value is always legal; it turns the corresponding acceleration off.

VariablePurposeEmpty means
MODEL_ROOTconvenience root a config can interpolate: MODEL_PATH=$MODEL_ROOT/<name>nothing: always set MODEL_PATH in full
NEW_VERL_DIRthe verl-swe_agent_opd_dev worktree carrying the veomni / R3 / async fixesrequired whenever USE_NEW_VERL=1
K8S_KUBECONFIGk8s backend clusterfalls back to $HOME/.kube/config
HARBOR_OPENSWE_IMAGE_REGISTRYprebuilt per-instance task imagesin-pod inline build (slow, needs egress)
HARBOR_NYDUS_MIRRORrewrites official docker.io/swebench/... val images to a local mirrorval images must be pullable from Docker Hub
HARBOR_HOSTPATH_MOUNTSJSON list of {host_path, mount_path, read_only} for offline grading assetsnull: no mounts
DOCKER_HOSTremote docker daemon (BACKEND=docker only)local socket
HARBOR_AGENT_RUNTIME_IMAGEmirror of the scaffold's agent runtime imagepulls docker.io/jierun/... directly

Image source is the main portability constraint

On k8s, force_build is ignored: KubernetesEnvironment only pulls, never builds. Without a prebuilt image in a reachable registry, or a Dockerfile whose FROM is pullable, every pod ends in ImagePullBackOffenv_setup_failed → reward 0. On docker, force_build=True works, but the daemon's builder needs egress. For real runs (hundreds to thousands of instances) prebuild and push; per-rollout dependency installs do not scale. A plain-HTTP registry must be trusted as insecure by every consumer: containerd certs.d/hosts.toml on each node, or insecure-registries in /etc/docker/daemon.json.

Outputs

A run writes, relative to the repo root:

OutputPathNotes
Checkpointscheckpoints/<project>/<exp>/global_step_N/actor FSDP shards, every TRAINER_SAVE_FREQ steps
Training loglogs/<exp>.logcarries the step:N - key:value metric lines
Throughput loglogs/<exp>_vllm.logthroughput only (the dashboard skips it)
Trajectoriesharbor_trials/<project>/<exp>/step_*/<session>/proxy_trajectory.jsonper-trial token ids / masks / logprobs
Curves / val resolve ratewandb + dashboardsee Dashboard

infer additionally writes RESULTS_DIR (per-trial results) and OUTPUT_INDEX (the selected-instances parquet); eval writes harbor trial dirs plus the score summary in its log. Layout details are in Results & Artifacts.

On this page