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.envThe 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:
| Layer | File | Scope | Changes |
|---|---|---|---|
| Site | scripts/lib/site.env | this cluster: registry, nydus mirror, hostPath mounts, kubeconfig, docker daemon, MODEL_ROOT, NEW_VERL_DIR | once, when you move clusters |
| Template | scripts/{train,eval,infer}/templates/*.env | a structural combination (mode × backend × scaffold × model) | never: you copy it |
| Config | scripts/{train,eval,infer}/configs/*.env | one experiment: names, data, topology, cadence | every 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.
| Axis | Values | What it switches |
|---|---|---|
TRAINING_MODE | async | sync | fully_async_main + fully_async_fsdp.yaml (split train/rollout pools) vs main_ppo + sync.yaml (one colocated pool) |
MODEL_ENGINE | veomni | fsdp | model_engine=veomni + veomni.* (required for hybrid-GDN models) vs strategy=fsdp2 + fsdp_config.*. Follows the model; see the table below |
SCAFFOLD | ohsdk | oh | cc | oc | agent class + runtime image + agent-loop config. ohsdk is primary; cc = Claude Code; oc = OpenCode |
BACKEND | k8s | docker | sandbox 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.
| Model | MODEL_ENGINE | SP_SIZE | Window (prompt + response) | TOOL_CALL_PARSER | ENABLE_R3 |
|---|---|---|---|---|---|
| Qwen3.5-35B-A3B (hybrid-GDN MoE) | veomni | 4 | 40 000 + 91 072 | qwen3_coder | auto → True |
| Qwen3-30B-A3B-Instruct-2507 (MoE) | veomni | 4 | 40 000 + 91 072 | by scaffold † | auto → True |
| Qwen3.6-27B (dense hybrid-mamba) | fsdp | 1 | 167 232 + 32 768 | qwen3_coder | auto → 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.
| Variable | Meaning |
|---|---|
PROJECT_NAME / EXP_NAME | wandb project + experiment name; also the checkpoints/ and harbor_trials/ path |
TRAIN_FILES / VAL_FILES | Task indexes: thin parquet rows pointing at Harbor task dirs |
TOTAL_EPOCHS | passes over the task index; _template.env marks it CHANGEME |
NNODES / N_NODES_TRAIN / N_NODES_ROLLOUT | topology; see below |
POD_NAME_PREFIX | label 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=1SP_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 updateAlgorithm 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 parallelismThe 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 config | What the runner reads |
|---|---|
SAVE_FREQ / TEST_FREQ / VAL_BEFORE_TRAIN | TRAINER_SAVE_FREQ / TRAINER_TEST_FREQ / TRAINER_VAL_BEFORE_TRAIN |
adv_estimator | ADV_ESTIMATOR |
policy_loss_mode | POLICY_LOSS_MODE |
learning_rate | ACTOR_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=disabledSite 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 imagesThe full surface. An empty value is always legal; it turns the corresponding acceleration off.
| Variable | Purpose | Empty means |
|---|---|---|
MODEL_ROOT | convenience root a config can interpolate: MODEL_PATH=$MODEL_ROOT/<name> | nothing: always set MODEL_PATH in full |
NEW_VERL_DIR | the verl-swe_agent_opd_dev worktree carrying the veomni / R3 / async fixes | required whenever USE_NEW_VERL=1 |
K8S_KUBECONFIG | k8s backend cluster | falls back to $HOME/.kube/config |
HARBOR_OPENSWE_IMAGE_REGISTRY | prebuilt per-instance task images | in-pod inline build (slow, needs egress) |
HARBOR_NYDUS_MIRROR | rewrites official docker.io/swebench/... val images to a local mirror | val images must be pullable from Docker Hub |
HARBOR_HOSTPATH_MOUNTS | JSON list of {host_path, mount_path, read_only} for offline grading assets | null: no mounts |
DOCKER_HOST | remote docker daemon (BACKEND=docker only) | local socket |
HARBOR_AGENT_RUNTIME_IMAGE | mirror of the scaffold's agent runtime image | pulls 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 ImagePullBackOff → env_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:
| Output | Path | Notes |
|---|---|---|
| Checkpoints | checkpoints/<project>/<exp>/global_step_N/ | actor FSDP shards, every TRAINER_SAVE_FREQ steps |
| Training log | logs/<exp>.log | carries the step:N - key:value metric lines |
| Throughput log | logs/<exp>_vllm.log | throughput only (the dashboard skips it) |
| Trajectories | harbor_trials/<project>/<exp>/step_*/<session>/proxy_trajectory.json | per-trial token ids / masks / logprobs |
| Curves / val resolve rate | wandb + dashboard | see 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.