YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Action Reward Models for Web Agents
Minimal, self-contained repo for the action reward model (ARM) study: generate per-step candidate-action data from a web-agent policy, train two kinds of reward models on teacher labels, and use them to pick actions at inference time. Everything here was extracted from two production pipelines ("eras") and trimmed to the essential path.
Written to be read by an AI assistant picking this up cold β file paths, gotchas, and provenance are spelled out. Author: Piotr Teterwak (piotr.teterwak@gmail.com). Models & data: https://huggingface.co/PTeterwak
The idea, in one paragraph
At every step, a web agent's policy samples n=5 candidate actions
(temp 0.7) instead of 1. A reward model picks which one to execute. Two RM
types, trained from the same teacher-selection labels: a selection ARM
(generative: sees all 5 candidates + screenshot, replies {"selection": N})
and a BradleyβTerry scalar RM (a value head scores each candidate
independently; argmax executes). Comparative judging beats absolute scoring,
and both beat n=1, across two actor families and even on desktop (OSWorld):
| actor / bench | n=1 | scalar BT | selection ARM | teacher (GPT-5.5) |
|---|---|---|---|---|
| MolmoWeb-4B / OM2W (gpt-5.2 judge) | 25.1% | 33.6% | 37.1% | β |
| OpenWebRL-4B-SFT / OM2W (o4-mini judge) | 33.8% | 46.3% | 51.1% | 47.1% |
| Qwen3.5-4B / OSWorld (369 desktop tasks) | 10.0% | β | β | 21.0% (GPT-5.5 as ARM) |
(The trained ARM beats its own GPT-5.5 teacher on OM2W. Judge models differ across rows β never compare absolute numbers across rows without a re-judge.)
Assets on Hugging Face
Models (all judge-base Qwen3.5-4B, LoRA unless noted):
| repo | type | actor era |
|---|---|---|
PTeterwak/OpenWebRL-4B-SelectionARM |
selection (merged full model) | OpenWebRL |
PTeterwak/OpenWebRL-4B-ScalarRM-LoRA |
BT scalar (adapter + value_head.safetensors) |
OpenWebRL |
PTeterwak/om2w-action-rm-selection-4b |
selection (merged) | MolmoWeb |
PTeterwak/om2w-action-rm-scalar-4b-lora |
BT scalar (adapter + value_head.pt) |
MolmoWeb |
Data: PTeterwak/action-reward-models-data β states, candidate sets,
teacher labels, and the built training sets for the OpenWebRL era (incl.
screenshots), plus the MolmoWeb-era BT pair files. Layout documented in the
dataset card.
Pipeline (what the scripts do, in order)
Stage 1 β data generation (data_generation/)
OpenWebRL actor (openwebrl_actor/, Aug 2026 era β the cleaner one):
extract_states.pyβ pull (prompt_text, screenshot) states from SFT trajectories. Output:states_full.jsonl+state_images/.sample_candidates.pyβ for each state, sample n=5 candidates at temp 0.7 from the actor (served via vLLM/sglang). Re-run each state multiple times with#drawsuffixed state_ids to scale the set (we did ~3k states β 49.5k sets). Designed as a claim-file fleet: any number of concurrent jobs share one work list viaO_CREAT|O_EXCLclaim files.build_teacher_batch.pyβ package each 5-candidate set into an OpenAI Batch API request with the selection prompt; the teacher (GPT-5.5) returns{"selection": N}+ reasoning. (~$150 for 40k labels; 99.8% parse.)build_selection_sft.pyβ labels β ShareGPT-format SFT set for LLaMA-Factory (image + 5 candidates β{"selection": N}target). 2% of label draws held out by seed-42 (rng.random() < 0.02) β every eval script reproduces this exact split; don't change the seed.build_scalar_rm_data.pyβ the same labels β BradleyβTerry pairs (teacher's pick vs each distinct loser, β€2 pairs/set β 76.7k pairs), LLaMA-Factoryranking: trueformat with chosen/rejected branches.
MolmoWeb actor (molmoweb_actor/, MayβJun 2026 era): same shape, older
plumbing. build_catts_distill_data.py / build_distill_selections.py build
selection training data from arbiter runs over MolmoWeb-sampled candidates;
build_reward_data.py + build_reward_pairs.py build pointwise scores and
compute-parity BT pairs (that era also ablated the label source: BT trained
on counterfactual-PRM labels vs on selection labels β see bt_* variants).
Stage 2 β training (training/)
LLaMA-Factory route (llamafactory/ β used for the OpenWebRL era; the
easiest to reproduce):
arm_lora.yamlβ selection ARM: stagesft, all-linear LoRA r=32 on the actor's own base, 2 epochs (loss 2.67 β 0.05, ~22h on 1Γ80GB). Thenarm_merge.yamlmerges to a full checkpoint for vLLM serving.scalar_rm_lora.yamlβ BT scalar: stagerm(LLaMA-Factory trains a value head with -logsigmoid(r_chosen β r_rejected)), LoRA r=32, 1 epoch. Held-out pairwise accuracy 75.8%. Gotchas: eval needseval_dataset:(notdataset:), batch size 1, 80GB GPU (scalar_rm_eval.yamlshows the working config); pip may resolve a CUDA-ABI-mismatched torchaudio β pin to your torch's CUDA.
Custom BT trainer (custom_bt/ β the MolmoWeb era's route):
train_reward.py with OBJECTIVE=bt (pairs jsonl in, LoRA + value head
out); run_train_reward*.qsub show the exact env/args used, and
chain_bt_run.sh the smoke-then-full launch pattern.
Stage 3 β inference (inference/)
selection_infer.pyβ the 60-second demo. Serve the selection ARM with vLLM, pass task + screenshot + candidates json, get{"selection": N}.scalar_infer.pyβ loads base + LoRA + value head directly (no server), scores each candidate, argmax.scalar_server.pyβ production-grade batched/scoreendpoint (the one the eval harnesses call), PRM'-format prompts for the MolmoWeb-era model (templates/prm2_templates.jsonis byte-exact to its training data).selection_prompt.pyβ the canonical selection prompt builder (catts_vision v2: pure vision, no DOM/SoM/votes/CoT, single shot). Both selection ARMs were trained on prompts from this builder with:CATTS_VISION_PROMPT_V2=1 CATTS_VISION_COLORED=1 VISION_NO_SOM=1 VISION_ABLATE_DOM=1 VISION_ABLATE_VOTES=1 NORMALIZE_COORDS=1 CLUSTER_NO_DOM=1 VISION_NO_COT=1. Prompt drift is the #1 way to get garbage numbers β use this builder, don't approximate it.
Cross-cutting gotchas (earned the hard way)
- Coordinates are normalized [0,1000] in candidate actions for both ARMs. Feeding pixel coords silently degrades selection quality.
- Selection index is 1-based in the
{"selection": N}contract; parse failures fall back to candidate 1 (do the same, it matters for parity). - Candidate diversity is the fuel: sample candidates at temp 0.7. Greedy candidates collapse to near-duplicates and selection becomes a no-op (~72% of steps have β₯2 genuinely distinct candidates at 0.7).
- The dedup question: we do NOT dedup candidates before selection (CLUSTER_NO_DOM=1 presents all 5). Strict-index metrics under-credit judges when duplicates exist β use action-level agreement for analysis.
- Judges differ across eras (gpt-5.2 / o4-mini / GPT-4.1 give spreads of 5β9 points on identical trajectories). Any new comparison table should re-judge everything with one judge.
- The scalar value head rides OUTSIDE the adapter weights β always ship
value_head.{safetensors,pt}next to the LoRA (both HF repos do).
Environments
Three pinned requirements files, split by concern (versions taken from the
actual working SCC envs, Sep 2026): requirements-inference.txt (torch 2.11 /
transformers 5.14 / vllm 0.26 β transformers must be >=5.x for qwen3_5),
requirements-training.txt (LLaMA-Factory from source; beware CUDA-mismatched
torchaudio), requirements-datagen.txt (CPU-side). Training and inference
were run from SEPARATE envs β the MolmoWeb actor pins transformers 4.57.x
while the Qwen3.5 RMs need >=5.x, so plan on two envs if you run both.
Serving checklist (vLLM JIT-compiles kernels at startup; every one of these was independently fatal in a bare batch shell, in this order):
peft+safetensorsinstalled in the serving env (scalar path).CUDA_HOMEset to a real CUDA >=12.8 install and$CUDA_HOME/binon PATH (absolute paths β HPCmodule loadcan silently no-op in non-interactive shells; don't trust it).- The conda/venv
binFIRST on PATH (vLLM's JIT needsninjafrom it). LD_LIBRARY_PATHcontaining$CUDA_HOME/lib64(JIT-built kernels dlopenlibcudart.so.12at runtime). Both demo paths were verified end-to-end with real weights (scalar: HF adapter + value head scored 5 candidates; selection: vLLM-served ARM returned{"selection": N}) under exactly this environment.
Repo layout
data_generation/
openwebrl_actor/ extract_states.py sample_candidates.py build_teacher_batch.py
build_selection_sft.py build_scalar_rm_data.py
molmoweb_actor/ build_catts_distill_data.py build_distill_selections.py
build_reward_data.py build_reward_pairs.py
training/
llamafactory/ arm_lora.yaml arm_merge.yaml scalar_rm_lora.yaml scalar_rm_eval.yaml
custom_bt/ train_reward.py run_train_reward*.qsub chain_bt_run.sh
requirements-{inference,training,datagen}.txt
inference/
selection_infer.py scalar_infer.py scalar_server.py selection_prompt.py
templates/prm2_templates.json
Provenance
Extracted from: CUA_evals/owrl_arm (OpenWebRL era, Aug 2026),
browser_agents/browser-environment + browser_agents/repro_v2_tree
(MolmoWeb era, MayβJul 2026; the repro tree also contains a fully
self-contained OM2W reproduction harness with pinned tasks). Dashboard with
all result tabs: https://weekly-dashboard-inky.vercel.app (tabs 5.20β9.1).