Training
Launch a PPO/GRPO/GSPO run over a Harbor task index
Trains a policy on real repository tasks. Each step samples instances from a task index, runs the agent against them in sandboxes, grades the result with each task's own test suite, and takes a gradient step on the resulting trajectories.
task index ─▶ [ training ] ─▶ checkpoints ─▶ evaluationPrerequisites
- A train and a val task index (parquet pointer tables). Difficulty-filtered indexes train faster; see Rollout Generation.
- Sandbox images for those tasks, in a registry the cluster can pull from.
- A policy checkpoint on local disk, typically SFT-trained.
- A
site.envdescribing the cluster, and the venv from Getting Started. - 8× GPU per node. Multi-node needs a reachable Ray head; see Scaling Up.
Setup
A run config starts as one template and replaces its CHANGEME values:
cp scripts/train/_template.env scripts/train/configs/my_run.envThe config does not pick a template file per axis — it sets four values, and
the runner composes the matching modules from scripts/templates/ at launch:
| Axis | Variable | Values |
|---|---|---|
| Sandbox backend | BACKEND | k8s docker |
| Agent scaffold | SCAFFOLD | ohsdk cc oc oh |
| Rollout placement | TRAINING_MODE | sync async |
| Training engine | MODEL_ENGINE | veomni fsdp |
Identity and topology
# scripts/train/configs/my_run.env
PROJECT_NAME=my-project
EXP_NAME=first-run
SCAFFOLD=ohsdk # cc | ohsdk | oc | terminus
BACKEND=k8s # k8s | docker
TRAINING_MODE=async # sync colocates rollout and training
MODEL_ENGINE=veomni # veomni for MoE, fsdp for dense
NNODES=3 # must equal the two below
N_NODES_TRAIN=2
N_NODES_ROLLOUT=1
SP_SIZE=8 # must divide the training worldPROJECT_NAME and EXP_NAME name every artifact the run writes, so pick them
before launching rather than renaming directories afterwards.
Model and data
MODEL_PATH=/models/Qwen3.5-35B-A3B
TOOL_CALL_PARSER=qwen3_coder # must match the model's chat template
TRAIN_FILES=/data/harbor_indexes/train.parquet
VAL_FILES=/data/harbor_indexes/val.parquetTOOL_CALL_PARSER mismatched against the model's chat template is the single most
expensive typo here: the agent emits tool calls the server cannot parse, every
trajectory ends early, and reward stays near zero for reasons that look like the
model's fault. Validation checks this pair.
Batch and context
TRAIN_BSZ=64 # prompts per step
N_RESP=8 # responses per prompt → 512 trials/step
MAX_PROMPT=30000
MAX_RESP=170000
TRAINER_SAVE_FREQ=10
TRAINER_TEST_FREQ=10Smoke first
On a new model, scaffold, or cluster, start from a smoke config (TRAIN_BSZ=8
N_RESP=4): ~8× faster per step, and it exercises the whole path. Its gradients
are noisy and its reward groups degenerate, so it is a plumbing check only,
never a learning-quality signal.
Everything else takes a default. Configuration lists every value a run reads and which layer each one belongs to.
Check
PREFLIGHT_ONLY=1 bash scripts/train/train.sh scripts/train/configs/my_run.env
# or: /rl:check scripts/train/configs/my_run.envThis resolves the config, prints the run configuration block, and runs every
assertion in Run Validation. It launches nothing and
stops on any ✗ FATAL. To see the fully expanded launch command instead:
DRY_RUN=1 bash scripts/train/train.sh scripts/train/configs/my_run.env/rl:check runs the same assertions and adds what a config-only script cannot
judge: whether a run is already in flight, whether those GPUs are yours.
Run
nohup setsid bash scripts/train/train.sh scripts/train/configs/my_run.env \
> logs/train_$(date +%m%d).out 2>&1 &
# or: /rl:run scripts/train/configs/my_run.envA run lasts days. Launch it under tmux or nohup setsid so it survives a shell
disconnect. For multi-node runs, start the same config on every node; the node
whose local IP matches the Ray head becomes rank 0 automatically. If a previous
run left Ray or vLLM processes behind, clear them first with
bash scripts/cleanup_before_run.sh.
The runner brings up Ray, boots vLLM, starts the in-process proxy, and drives the PPO/GRPO/GSPO loop. On a 30B MoE at TP=4, vLLM CUDA-graph capture takes 10–20 minutes before the first replica registers.
Reference deployment
The numbers below are observations from our internal reference cluster, provided
for capacity planning — not performance guarantees. Both runs: Qwen3.5-35B-A3B on
veomni, TRAIN_BSZ=64 N_RESP=8, 8 GPUs per node (≥100 GB per device), vLLM
0.19, torch 2.10, Kubernetes sandboxes pulling prebuilt images from a local
registry. Your throughput will differ with hardware, task mix and scaffold.
| Run | Nodes | Context | timing_s/step | Peak GPU memory |
|---|---|---|---|---|
| OH-SDK scaffold | 3 (2 train + 1 rollout) | 128k | ~3 900 s (~65 min) | 95.7 GB |
| Claude Code scaffold | 4 (3 train + 1 rollout) | 200k | 6 800–8 300 s (~2 h) | 99.1 GB |
Rollout dominates: timing_s/gen is 70–80% of the step in both. Adding rollout
nodes, not training nodes, is what shortens a step.
First step
The first step decides whether the rest of the run is worth its GPU hours. Three numbers, in the order they appear:
| Signal | Healthy | If it is wrong |
|---|---|---|
router_replay/pearson (MoE only) | ≈ 0.999 | rollout and training disagree on expert routing; the gradients are noise |
actor/lr | the configured value | a cosine schedule with total_training_steps=-1 collapses to 0 and the model never learns |
critic/rewards/mean | non-zero within a step or two | an all-zero reward is almost always infrastructure, not the model; see Rewards & Throughput |
tail -F logs/<exp_name>.log
# or: /rl:statusOutput
logs/<exp>.log step metric lines
logs/<exp>_vllm.log throughput-only log
harbor_trials/<project>/<exp>/step_*/<session>/ per-trial trajectories
checkpoints/<project>/<exp>/global_step_N/ actor shards, every TRAINER_SAVE_FREQA checkpoint is FSDP/veomni shards, not a servable model. Merge one to HuggingFace format before scoring it:
python -m verl.model_merger merge \
--backend fsdp \
--local_dir checkpoints/<project>/<exp>/global_step_30/actor \
--target_dir /models/checkpoints/global_step_30_hfThat directory is the MODEL_PATH of an Evaluation
run. Results & Artifacts documents
every stream in full.
Dashboard
bash webui/start_dashboard.sh # http://<host>:8090 (+ public tunnel)
# or: /rl:dashboardReward, KL, entropy, response-length and MFU curves, plus a browser over the
per-trial trajectory JSON. It reads logs/ and harbor_trials/ directly, so it
works on a finished run too. See Monitoring.
Cleanup
bash scripts/cleanup_before_run.sh # ray + vLLM + litellm workers, orphan pods, /tmpSafe to re-run, and safe when nothing is up. Run it after any run that did not exit cleanly: orphaned litellm workers keep holding port 8002, and the next launch's health probe is happy to talk to a proxy whose vLLM backends are gone.
Checkpoints stay where verl wrote them. On a finished run, deleting the optimizer
shards and keeping model_world_size_* frees roughly two thirds of the space —
the merger only reads the model shards.
Parameters
| Variable | Default | Meaning |
|---|---|---|
TRAINING_MODE | async | sync colocates rollout and training; async separates them |
MODEL_ENGINE | veomni | veomni for MoE policies, fsdp for dense |
TRAIN_FILES / VAL_FILES | — | task indexes, required |
NNODES | — | must equal N_NODES_TRAIN + N_NODES_ROLLOUT |
SP_SIZE | 8 | Ulysses sequence parallelism; must divide the training world |
TRAIN_BSZ / N_RESP | 64 / 8 | prompts per step × responses per prompt = trials per step |
MAX_PROMPT / MAX_RESP | 30000 / 170000 | context window split |
ENABLE_R3 | derived | routing replay; defaults to whether MODEL_PATH is MoE |
TRAINER_SAVE_FREQ / TRAINER_TEST_FREQ | 10 / 10 | checkpoint and validation cadence |