SWE-Lego-RL

Reference

Configuration

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

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

bash scripts/train/train.sh  train/configs/my_run.env
bash scripts/eval/eval.sh    eval/configs/my_eval.env
bash scripts/infer/infer.sh  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.

Legacy scripts

scripts/sync_1node_cc.sh, scripts/fully_async_*.sh, scripts/eval_*.sh, and scripts/infer_*.sh still run. They spell out every knob inline, so a fix in one does not reach the others. 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

lib/common_env.sh sources site.env if it exists. No cluster-specific value is hardcoded in the runners or libs: with no site.env the stack falls back to a portable path — in-pod inline build, no mounts, no image rewrite. That path is slower but runs anywhere.

Do not share your site.env

lib/site.env holds your cluster's registry addresses, kubeconfig path, and mounts. Copy lib/site.example.env when onboarding a new cluster.

Parameter tiers

TierWho changes itExamplesLives in
T0 axespicked once, by choosing a templateTRAIN_MODE SCAFFOLD BACKEND MODEL_PRESETtemplate filename + first lines
T1 requiredevery runPROJECT_NAME EXP_TAG TRAIN_INDEX/VAL_INDEX topologythe CHANGEME lines
T2 often tunedfrequentlySAVE_FREQ TOTAL_EPOCHS TRAIN_BSZ N_RESP MAX_RESP VAL_BEFORE_TRAIN N_CONCURRENTconfig, add a line
T3 advancedrarely, and know whySP_SIZE ENABLE_R3 ROLLOUT_IS TRAJ_FILTER_* GPU_MEM_UTIL KL_LOSS_COEFconfig, overrides a default
T4 siteonce per clusterregistry / nydus / mounts / kubeconfig / MODEL_ROOT / NEW_VERL_DIR / DOCKER_HOSTlib/site.env
T5 hiddenbasically never~80 harbor + hyperparameter defaults (loop config, offload, tail-kill, pod timeouts…)lib/* + the runner

Templates carry T0–T1 plus a few T2 examples. Setting a T2/T3 knob is one KEY=VALUE line in the config. Multi-word values must be quoted (EVAL_VLLM_EXTRA_ARGS="--enforce-eager --dtype bfloat16").

Axes

Only these four require a different template. Everything else is a parameter.

AxisValuesWhat it switches
TRAIN_MODEasync | syncfully_async_main + fully_async_fsdp.yaml (split train/rollout pools) vs main_ppo + sync.yaml (one colocated pool)
ENGINEveomni | fsdpmodel_engine=veomni + veomni.* (required for hybrid-GDN models) vs strategy=fsdp2 + fsdp_config.*. Set by the model preset — normally leave it alone
SCAFFOLDohsdk | oh | cc | ocagent class + runtime image + agent-loop config. ohsdk is primary; cc = Claude Code; oc = OpenCode
BACKENDk8s | dockersandbox backend — see Backends
MODEL_PRESETsee belowfills MODEL_PATH TOOL_PARSER ENGINE SP_SIZE + context window

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

Model presets

A preset fills a slot of correlated values, so switching models is a one-line change. It only fills what the config left unset — an explicit MODEL_PATH always wins.

PresetModelEngineSP_SIZEWindow (prompt + response)TOOL_PARSER
qwen35a3bQwen3.5-35B-A3B (hybrid-GDN MoE)veomni440 000 + 91 072qwen3_coder
qwen3-30b-a3bQwen3-30B-A3B-Instruct-2507 (MoE)veomni440 000 + 91 072by scaffold †
qwen36-27bQwen3.6-27B (dense hybrid-mamba)fsdp1167 232 + 32 768qwen3_coder

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. The preset resolves this from SCAFFOLD and preflight re-checks it; a wrong value means a 100% tool-parse failure rate on step 0 (see Tool-call parser mismatch).

For a model outside the table, leave MODEL_PRESET empty and set MODEL_PATH, TOOL_PARSER, ENGINE, SP_SIZE, MAX_PROMPT, and MAX_RESP explicitly.

Required fields

trainPROJECT_NAME · EXP_TAG · TRAIN_INDEX · VAL_INDEX · topology:

VariableMeaning
PROJECT_NAME / EXP_TAGwandb project + experiment name; also the checkpoints/ and harbor_trials/ path
TRAIN_INDEX / VAL_INDEXTask indexes — thin parquet rows pointing at Harbor task dirs
NNODES / N_NODES_TRAIN / N_NODES_ROLLOUTtopology. NNODES must equal the sum; preflight is fatal otherwise, because a mismatched Ray cluster hangs in placement
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 knobs: 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 TRAIN_INDEX 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.

Common edits

One line each, in the run config (scripts/{train,eval,infer}/configs/*.env):

MODEL_PRESET=qwen36-27b        # switch policy model
# MODEL_PATH=/models/my-model TOOL_PARSER=qwen3_coder SP_SIZE=1   # model outside the preset table

SCAFFOLD=cc                    # ohsdk | oh | cc | oc
TRAIN_MODE=async               # sync ↔ async — also copy the matching template (entrypoint + YAML change)
BACKEND=docker                 # Docker sandboxes — see /docs/run-training/backends

NNODES=3                       # add nodes: NNODES must equal the sum
N_NODES_TRAIN=2
N_NODES_ROLLOUT=1

adv_estimator=grpo             # algorithm: ppo | grpo
policy_loss_mode=gspo

N_CONCURRENT=32                # rollout parallelism
TRAIN_BSZ=8 N_RESP=4           # smoke run (plumbing check, not a learning signal)

MODEL_PATH=/ckpt/global_step_30_hf   # eval a checkpoint (in an eval config)
WANDB_MODE=disabled            # opt out of wandb

Override a single run without editing any file:

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.

Site variables

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

K8S_KUBECONFIG=/path/to/kubeconfig.yaml        # which K8s cluster
MODEL_ROOT=/models                             # where presets resolve model paths
NEW_VERL_DIR=/repos/verl-swe_agent_opd_dev     # required with USE_NEW_VERL=1
HARBOR_OPENSWE_IMAGE_REGISTRY=reg:5001/openswe # prebuilt task images
HARBOR_NYDUS_MIRROR=...                        # mirror official val images
DOCKER_HOST=tcp://10.0.0.2:2376                # remote docker daemon

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

VariablePurposeEmpty means
MODEL_ROOTwhere model_presets looks up <model-name>presets cannot resolve paths — set MODEL_PATH yourself
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

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.

Hyperparameters

The default profile is GRPO advantages with a GSPO policy loss, lr 1e-6, and 64 × 8 = 512 trials per step. The context window comes from the model preset. To move off the defaults:

adv_estimator=grpo            # ppo | grpo
policy_loss_mode=gspo         # sequence-level policy loss
learning_rate=1.0e-06
LR_SCHEDULER=constant         # keep constant on fully-async (see preflight)
TRAIN_BSZ=64
N_RESP=8                      # 64 × 8 = 512 trials/step
MAX_PROMPT=40000
MAX_RESP=91072                # window = prompt + response
SAVE_FREQ=5
TOTAL_EPOCHS=3
TEMPERATURE=1.0

The meaning of each symbol is defined in Core Concepts.

wandb

The API key never belongs in a config file:

export WANDB_API_KEY=...       # in your shell, before launch
# or: WANDB_MODE=disabled

Outputs

A run writes, relative to the repo root:

OutputPathNotes
Checkpointscheckpoints/<project>/<exp>/global_step_N/actor FSDP shards, every 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