Dataset comparison suite
June 8, 2026 · View on GitHub
Scripts & code for comparing our RL dataset (tasks_skill_tax_20260401_10k)
against terminal-task baselines — currently
obiwan96/endless-terminals,
open-thoughts/OpenThoughts-Agent-v1-RL,
ucsb-mlsec/terminal-bench-env
(the TermiGen / Harbor 2.0 task bank, ~3.5k tasks),
m-a-p/TerminalTraj-5k-instances
(the TerminalTraj 5k-instance release, TerminalBench 1.0 layout, 5,660 tasks),
hamishivi/agent-task-cli-gym
(CLI-Gym, SWE-Smith environment-inversion repair tasks, 1,552 → ~1,452 verifiable),
and hamishivi/agent-task-swe-smith
(SWE-smith, SWE-bench-style synthetic bug-repair tasks, ~59k).
TL;DR
# Full pipeline: ingest -> classify -> solve -> compare
bash rl_data/scripts/comparison/run_comparison.sh
# Cost-bounded run: solve only 250 randomly-sampled ET tasks
SAMPLE_SIZE=250 SAMPLE_SEED=42 bash rl_data/scripts/comparison/run_comparison.sh
# Analysis only (solves already done)
SKIP_INGEST_ET=1 SKIP_INGEST_OT=1 SKIP_CLASSIFY=1 \
SKIP_SOLVE_ET=1 SKIP_SOLVE_OT=1 \
bash rl_data/scripts/comparison/run_comparison.sh
Files
Python package — rl_data/comparison/
rl_data/comparison/
core.py # DatasetSpec + load_records + save_fig_with_data + plot helpers
modules.py # all 6 analysis functions
command_taxonomy.py # bash-command classifier (16 categories)
taxonomy_classifier.py # LLM-based domain/skill_type/task_complexity/command_complexity
cli.py # `python -m rl_data.comparison.cli`
adapters/
skill_tax.py # identity
endless_terminals.py # obiwan96/endless-terminals (HF)
openthoughts_tb.py # open-thoughts/OpenThoughts-TB-dev (HF)
openthoughts_agent_rl.py # open-thoughts/OpenThoughts-Agent-v1-RL (HF, parquet tarballs)
termigen.py # ucsb-mlsec/terminal-bench-env (GitHub, Harbor 2.0)
terminaltraj.py # m-a-p/TerminalTraj-5k-instances (HF, tarball, TB 1.0)
r2e_gym.py # hamishivi/agent-task-r2e-gym (HF, tarball, per-task image)
cli_gym.py # hamishivi/agent-task-cli-gym (HF parquet + LiberCoders verifier join)
swe_smith.py # hamishivi/agent-task-swe-smith (HF parquet + SWE-bench/SWE-smith bug+verifier join)
Shell launchers — rl_data/scripts/comparison/
comparison/
run_ingest_et.sh
run_ingest_openthoughts.sh
run_ingest_termigen.sh
run_ingest_terminaltraj.sh
run_ingest_r2e_gym.sh
run_ingest_cli_gym.sh
run_ingest_swe_smith.sh
run_classify_taxonomy.sh
run_generate_solutions_et.sh
run_generate_solutions_openthoughts.sh
run_generate_solutions_termigen.sh
run_generate_solutions_terminaltraj.sh
run_generate_solutions_r2e_gym.sh
run_generate_solutions_cli_gym.sh
run_generate_solutions_swe_smith.sh
run_comparison.sh # top-level orchestrator
COMPARISON.md # this file
Adding a new baseline
-
Drop a new adapter in
rl_data/comparison/adapters/<my_ds>.py:from rl_data.comparison.adapters import Adapter, flatten_harbor_task, register_adapter class MyDatasetAdapter(Adapter): name = "my_ds" hf_repo_id = "org/my-dataset" default_dst = "rl_data/output/tasks_my_ds" def convert_one(self, src, dst_root): return flatten_harbor_task(src, dst_root, source_name="myds", source_repo=self.hf_repo_id) register_adapter(MyDatasetAdapter()) -
Add the import in
rl_data/comparison/adapters/__init__.py. -
Add a
scripts/comparison/run_ingest_my_ds.shshell launcher. -
Pass it as another
--baselineto the CLI.
Modules already accept a list of N datasets — no changes needed on the analysis side.
Baseline-specific quirks
- TermiGen (
ucsb-mlsec/terminal-bench-env, ~3.5k tasks): lives on GitHub, not HF. The adapter does a--filter=blob:none --sparseclone of onlyenvironments_harbor/so we skip the largetermigen_env.zip(TB-1.0 artifact, unused). Every upstream DockerfileFROMsghcr.io/laude-institute/t-bench/ubuntu-24-04:20250624; the solve script prebuilds a sharedtbench_ubuntu24_base.sifonce and the adapter rewrites each per-taskcontainer.def'sBootstrap:/From:header tolocalimage/./tbench_ubuntu24_base.sifso per-task builds only layer the task-specific delta (payload files + any extra apt/pip beyond the commonpython3-pip/pytest/pandas/scipythat the base bakes in). The real verifier istests/test_outputs.py(the siblingtests/test.shis just a Harbor-style reward-logging wrapper aroundpyteston that file, which our harness runs directly). - OpenThoughts-Agent-v1-RL: tarballed inside a single
tasks.parquet; verifier istests/test.sh, wrapped into a pytest adapter that invokesbash /tests/test.sh. See adapter docstring for the instruction rewrite that fixes the upstream "fabricate your own fixtures" prompt. - Endless-Terminals: ships native
container.defs withFrom: ./ubuntu_22.04.sif; the ET solve script prebuilds that base SIF with python3/pip/pytest preloaded. - TerminalTraj (
m-a-p/TerminalTraj-5k-instances, 5,660 tasks): shipped as a single 13 MB5k_instances.tar.gzon HF; the adapter useshf_hub_download+tarfile.extractall, notsnapshot_download. Uses the TerminalBench 1.0 layout (task root withDockerfile,task.yaml,tests/test_outputs.py) — not Harbor 2.0 — soflatten_harbor_taskdoesn't apply; we write a TB-1.0–specific converter. Every taskFROMs a uniqueyizhilll/tb_container-<md5>:tmux_asciinema_v2image on Docker Hub (~400 MB each, all 5,660 distinct), so we can't pre-bake a shared base SIF the way we do for TermiGen/OT; per-task Docker pulls are unavoidable. The images span many distros (Debian/ Ubuntu/Fedora/Alpine/…) with varying Python+pip availability, so the adapter injects a robust pytest bootstrap into%postthat tries, in order: existingpip3→ system package manager →get-pip.py. Separately, a few tasks (e.g. Fedora 27 with glibc<2.33) fail with Apptainer's bundledfakerootbinary; the solve script therefore pre-builds each per-task SIF with--ignore-fakeroot-command(whichgenerate_solutions.build_sif()doesn't pass) before handing off to the harness. Nativecategory/difficulty/tagsintask.yamlare placeholder constants (mathematics/easy/["mathematics"]) across all 5,660 tasks, so domain/skill-type buckets rely entirely on the LLM classifier output. - CLI-Gym (
hamishivi/agent-task-cli-gym, 1,552 tasks → ~1,452 ingested): SWE-Smith environment-inversion repair tasks. Each base is a real Python repo (faker/pandas/scrapy/…) installed at/testbedinside a conda env namedtestbed; the published image has had its environment deliberately corrupted (e.g. swapped glibc localelanguage/territoryfields, poisoned codec registry, truncated shared lib) so a chosen subset of unit tests fails, and the agent must restore it. The verifier is not in the hamishivi parquet (itsdataset=passthrough/swerl_vanillux_sandboxenv defers reward to the environment), so the adapter loads two HF datasets and joins ontask_id: images + prompts (the instruction, which is byte-identical to the upstream task.yaml) fromhamishivi/agent-task-cli-gym, and the per-task run-tests.sh (the selected fail-to-pass + pass-to-pass unit tests) from the upstream releaseLiberCoders/CLI-Gym. (We read the instruction from the hamishivi prompt rather than the task.yaml because ~6 % of upstream task.yaml files have a malformedinstruction: |block scalar; the prompt text is equivalent and always well-formed.) Like R2E Gym / TerminalTraj, every taskFROMs its own pre-builthamishi740/agent-task-cli-gym:<hash>image (~900 MB, all distinct, public on Docker Hub), so no shared base SIF; the solve script pre-builds each with--ignore-fakeroot-command.test_final_state.pyactivates thetestbedconda env and runspytest <selected UTs>, passing iff all selected tests pass (= SWE-benchResolvedStatus.FULL); the agent's environment fixes survive into the verifier because the harness holds one--writable-tmpfsinstance across the rollout and the final test. Because the harness's outerpytest pytest_final_state.pyruns from the base conda PATH (which ships no pytest), the adapter's%postinstalls/exposes a pytest there purely so the wrapper can be collected — the real run happens intestbed. ~6 % of CLI-Gym tasks select no explicit tests (whole-suite run); those are skipped as unverifiable. Native domain/difficulty metadata is absent, so taxonomy buckets rely entirely on the LLM classifier output. - SWE-smith (
hamishivi/agent-task-swe-smith, ~59k tasks): SWE-bench-style synthetic bug-repair tasks. Each base is a real Python repo (the samejyangballin/swesmith.x86_64.<repo>.<sha>family CLI-Gym builds on) installed editable at/testbedinside a conda env namedtestbed. SWE-smith generates thousands of synthetic bugs per repo by procedurally corrupting the source code; the agent must repair the source so a set of broken unit tests (FAIL_TO_PASS) passes again. The verifier is not in the hamishivi parquet (dataset=passthrough/swerl_vanillux_sandbox), so the adapter loads two HF datasets and joins on the instance slug (task_id==instance_id): images + prompts (the SWE-smith problem statement) fromhamishivi/agent-task-swe-smith, and the per-instance bug patch +FAIL_TO_PASS/PASS_TO_PASSfrom the upstream releaseSWE-bench/SWE-smith. Key difference from CLI-Gym: thejyangballin/...base image is shared across every bug instance of a<repo>.<sha>(one image, thousands of tasks) and ships the clean repo, so the per-instance bug is not baked in — the adapter ships the datasetpatch(which, per the SWE-smith docs, is "the diff that creates the bug") into the SIF andgit applys it in%post, soFAIL_TO_PASSstarts red. A task whose bug patch fails to apply produces nocontainer.sifand is skipped (better than shipping a trivially-passing task); tasks with an empty bug patch or emptyFAIL_TO_PASSare dropped at ingest.test_final_state.pyactivates thetestbedconda env and runspytest <FAIL_TO_PASS>, passing iff all of them pass; the ~500-670PASS_TO_PASSno-regression tests are baked in too and additionally enforced whenAPPTAINERENV_SWE_SMITH_CHECK_P2P=1(off by default to bound runtime, mirroring CLI-Gym's selected-test verifier). As with CLI-Gym, the agent's source edits survive into the verifier via the harness's single--writable-tmpfsinstance, and the%postexposes a base-conda pytest purely so the outer wrapper can be collected. Native domain/difficulty metadata is absent, so taxonomy buckets rely entirely on the LLM classifier output.
Output layout (rl_data/output/comparison/)
Each figure ships with a co-located .csv holding the raw numbers so you can
replot in a custom style.
main/ # paper body (minimal)
fig1_difficulty_headline.png + .csv
fig2_command_mix_coverage.png + .csv
fig3_composition_domain.png + .csv
summary_table.md
summary_data.csv # machine-readable mirror
paper_snippets.md
appendix/ # deep-dive
difficulty_pass_at_k_overlay.png + .csv
difficulty_turn_cdf.png + .csv
command_mix_distinct_categories_hist.png + .csv
command_mix_turn_distribution.png + .csv
command_mix_cooccurrence_<dataset>.png + .csv
composition_skill_type.png + .csv # second-level taxonomy axis
composition_task_complexity.png + .csv
composition_command_complexity.png + .csv
diversity_shared_clusters.png + .csv
diversity_clusters.csv
realism_histograms.png + .csv
verifier_assertion_types.png + .csv
verifier_loc_asserts.png + .csv
per_task_metrics.csv
report.json # full machine-readable dump
Modules
- difficulty — pass@1, pass@8, turns, tokens, cost per task, Mann–Whitney U.
- command_mix — every bash tool call is tagged via
command_taxonomy.classify_one(16 categories: file manip, code_write, code_run, pkg_install, service, db, net, ...); reports coverage, per-turn distribution, co-occurrence heatmaps. Uses no metadata → fully fair. - composition — projects each dataset onto OUR taxonomy on four axes
(9 domains, ~29 skill_types, task complexity, command complexity) via the
LLM classifier.
fig3_composition_domain.pngis the main-body chart; skill_type / complexity charts are in the appendix. - diversity — shared TF-IDF clustering on the union corpus.
- realism — description length, apt+pip package counts, services started, artifacts checked by verifier.
- verifier —
ast-walkstest_final_state.py: LOC, assert count, assertion-type distribution.
Cost-bounded solve (SAMPLE_SIZE)
All four run_generate_solutions_*.sh scripts honour a SAMPLE_SIZE env
var that randomly subsamples tasks before solving, useful when you want
quick-turn comparisons without spending $30+:
# Solve only 250 randomly-sampled ET tasks with seed 42
SAMPLE_SIZE=250 SAMPLE_SEED=42 \
bash rl_data/scripts/comparison/run_generate_solutions_et.sh
The sample is drawn from the filtered task set and is deterministic in
SAMPLE_SEED so reruns pick the same subset.
pass@k breadth (NUM_SOLUTIONS)
All four solve scripts accept NUM_SOLUTIONS as an env override. The
harness's run_n_solutions() reports pass@k for every k in [1..N], so
NUM_SOLUTIONS=8 gives you both pass@1 and pass@8 (and 2/3/4/5/6/7) in
one run. Default is 1 (matches the 10k gemini run).
# Pass@8 ET run with the API model
NUM_SOLUTIONS=8 sbatch rl_data/scripts/comparison/run_generate_solutions_et.sh
For local-model pass@k runs, see "Local models (vLLM / Ollama)" below.
Local models (vLLM / Ollama)
The solver + classifier both use litellm, which supports local models via
OpenAI-compatible proxies out of the box. There are now two supported
flows: (A) one SLURM job that serves vLLM AND solves (recommended), or
(B) split serve and solve into two separate jobs (legacy).
A. Single-job, in-job vLLM (recommended for pass@k with a local model)
Each run_generate_solutions_*.sh accepts LAUNCH_VLLM=1, which makes the
script bring up vLLM on the same node it allocates, wait for readiness, run
the solver against http://127.0.0.1:<auto>/v1, and tear vLLM down on exit.
For a single dataset:
LAUNCH_VLLM=1 NUM_SOLUTIONS=8 \
sbatch rl_data/scripts/comparison/run_generate_solutions_et.sh
That single command gives you both pass@1 and pass@8 for ET under
Qwen/Qwen3-8B (the helper's default), with _summary.json filenames keyed
by model so existing gemini summaries are not overwritten.
For all four baselines under one shared vLLM (single 8xH200 allocation, single weight load, no idle GPUs between datasets), use the orchestrator:
APPTAINER_DOCKER_USERNAME=... APPTAINER_DOCKER_PASSWORD=... \
sbatch rl_data/scripts/comparison/run_local_qwen3_pass_at_8.sh
Useful overrides:
# Subset of datasets
DATASETS="et openthoughts" sbatch rl_data/scripts/comparison/run_local_qwen3_pass_at_8.sh
# Cost-bounded (250 random tasks per baseline)
SAMPLE_SIZE=250 SAMPLE_SEED=42 sbatch rl_data/scripts/comparison/run_local_qwen3_pass_at_8.sh
# Different model (Qwen2.5-Coder-7B-Instruct)
VLLM_MODEL=Qwen/Qwen2.5-Coder-7B-Instruct \
sbatch rl_data/scripts/comparison/run_local_qwen3_pass_at_8.sh
# Different pass@k breadth
NUM_SOLUTIONS=16 sbatch rl_data/scripts/comparison/run_local_qwen3_pass_at_8.sh
The vLLM helper (_vllm_local.sh) auto-picks tensor-parallel = 1 and
data-parallel = visible GPU count, which is the throughput-optimal config
for an 8B model that fits on one H200. Override via VLLM_TP / VLLM_DP.
For Qwen3-family models the helper additionally:
- enables
--tool-call-parser hermes(OpenAI-compatible tool calls in our bash-tool agent loop), - enables
--reasoning-parser qwen3(so<think>tokens go toreasoning_content, notcontent), - sets
--chat-template-kwargs '{"enable_thinking": false}'to skip the thinking pass entirely (much faster turns; opt back in by exportingVLLM_DISABLE_THINKING=0).
B. Two-job (separate vLLM + solver) — legacy
If you'd rather keep vLLM on one node and run solvers from another, the older two-job recipe still works:
-
Launch vLLM on a GPU node (one-liner helper):
MODEL=Qwen/Qwen2.5-Coder-7B-Instruct TP=1 \ bash rl_data/scripts/generate_solutions/launch_vllm.sh -
Point the solve script at it:
export MODEL="hosted_vllm/Qwen/Qwen2.5-Coder-7B-Instruct" export HOSTED_VLLM_API_BASE="http://<vllm-host>:8000/v1" bash rl_data/scripts/comparison/run_generate_solutions_et.sh
The solve scripts detect HOSTED_VLLM_API_BASE / OLLAMA_API_BASE /
OPENAI_API_BASE and arrange the right env for litellm.completion. No
code changes required.
Ollama variant:
export MODEL="ollama_chat/qwen2.5-coder:7b"
export OLLAMA_API_BASE="http://localhost:11434"
Note: tool-calling (our bash-tool loop) requires the local backend support
OpenAI-style tool_calls. vLLM supports this via --tool-call-parser hermes for Qwen2.5/Qwen3 (the launcher enables this by default).
Dependencies
uv pip install scikit-learn scipy
uv pip install vllm==0.19.1 # only if running a local server
scipy powers Mann-Whitney U + chi-squared; scikit-learn powers TF-IDF + KMeans in the diversity module. Both are gated — missing them logs a warning and those sub-modules no-op rather than crashing.
Rough cost estimate (API flash, full-run)
- Ingest: CPU-only, ~10 min for ~2500 ET tasks + ~730 OT-Agent-RL tasks +
~3500 TermiGen tasks + ~5660 TerminalTraj tasks + ~1450 CLI-Gym tasks.
TermiGen's first run does a ~350 MB sparse clone of the GitHub repo;
TerminalTraj's first run pulls a single 13 MB tarball from HF; CLI-Gym
pulls two small parquet datasets (hamishivi + LiberCoders) via
datasets.load_datasetand joins them in memory (no image pulls at ingest — those happen in the solve pre-build). SWE-smith similarly joins two parquet datasets in memory, but the verifier source (SWE-bench/SWE-smith) is ~4 GB (11 shards, ~52k bug patches), so the first ingest is download-bound (~5-15 min on a fast link, cached after). - Taxonomy classifier: ~$5-8 total (~12k × one flash call each).
- Solves (NUM_SOLUTIONS=1, ~30k tokens/task): ~$30-60 for ET, <$5 for
OT-Agent-RL, ~$40-80 for TermiGen,
$5-10 for TerminalTraj at default$60-100 for the full 5660-task run). Baseline cost scales linearly withSAMPLE_SIZE=500(NUM_SOLUTIONS. - TerminalTraj disk budget: each per-task SIF is ~500 MB, so 500
tasks ≈ 250 GB, 5660 ≈ 2.8 TB. Bound via
SAMPLE_SIZE. - CLI-Gym disk budget: each per-task SIF is ~900 MB, so 250 tasks
≈ 225 GB, ~1,452 ≈ 1.3 TB. Bound via
SAMPLE_SIZE(default 250). - SWE-smith disk budget: each per-task SIF is ~900 MB, so 250 tasks
≈ 225 GB. Bound via
SAMPLE_SIZE(default 250). Many sampled tasks share a<repo>.<sha>base layer, so apptainer's layer cache reduces the effective pull volume below the naive per-SIF figure. - Comparison post-processing: <10 min CPU on 10k + ~12k tasks.
Using SAMPLE_SIZE=250 cuts any solve to ~10% of the above. Using a
local Qwen model cuts it to zero (modulo GPU wall time).