Codebase for experiments on unexploitable search. This repo studies a new generative adversarial RL method which works without demonstrations. The goal is to enforce diversity at a semantic level.
The core method is a two-player game (GAP — Game-based Adversarial Properties): a proposer (Alice) solves the task while an adversary (Charlie) identifies non-essential properties of Alice's solutions and proposes alternatives without them. Alice is rewarded for solving the task in ways the adversary cannot pin down to a single exploitable pattern — pushing the policy toward a semantically diverse set of solutions. The game needs no human demonstrations of "diverse" behaviour: the adversary generates the contrast set on the fly.
bash setup.sh # installs uv, runs uv sync
Requires:
podman-hpc (or Docker) for sandboxed code execution (APPS domain)Environment variables:
PROJECT_SHARED — root for multirun/output dirs (defaults to .)WANDB_MODE — offline (default) or onlinePYTHON_TMPDIR — temp directory for model downloadsunexploitable-search/
├── conf/ # Hydra configs (YAML)
│ ├── _shared/ # Shared config fragments
│ │ ├── adapter/lora.yaml # LoRA: r=32, alpha=16
│ │ ├── it_model/ # Base models (qwen3_8b, llama3_8b, gemma3_12b, …)
│ │ ├── model_organism/ # Starting-model loaders (prompted, trained, trained_hf)
│ │ └── setting/ # Task domains (apps, alpaca, toy)
│ ├── mitigation_strats/ # RL training config
│ │ ├── config.yaml # Main config
│ │ └── strat/ # Strategy selection (none, game, hard_game,
│ │ │ # entropy_bonus, kl_from_base_penalty, length_penalty)
│ │ └── trainer_config/ # GRPO trainer hyperparams
│ └── evals/ # Evaluation configs (benign task-correctness)
│
├── scripts/ # User-facing entry points
│ ├── train/
│ │ ├── train_with_mitigation_strat.py
│ │ └── gpu_queue_runner.py # Multi-GPU job queue
│ ├── eval/
│ │ ├── run_benign_eval.py # Task-correctness eval
│ │ ├── run_toy_baseline.py # Toy: baseline characterisation
│ │ ├── run_toy_diversity_eval.py # Toy: diversity eval on checkpoints
│ │ └── generate_bon_reference.py # BoN reference generation (toy)
│ ├── experiments/
│ │ ├── toy_0.6b/ # Toy 0.6B experiment batch scripts
│ │ ├── toy_1.7b/ # Toy 1.7B experiment (Qwen3-1.7B)
│ │ └── qwen3_8b_base/ # Q8B-Base diversity benchmark
│ ├── slurm/ # Slurm job helpers
│ │ ├── auto_retry.sh # Failure-mode-aware retry with checkpoint resume
│ │ └── podman_storage.sh # Fix podman-hpc storage on unreliable nodes
│ ├── lib/ # Shared analysis utilities
│ │ └── experiment_discovery.py # Auto-discover experiment arms & trajectories
│ ├── debug/ # Profiling & competence checks
│ ├── plots/ # Plotting utilities
│ ├── experiment_status.py # Show experiment arm status (DONE/RUN/MISS)
│ ├── analyze_trajectories.py # JSONL trajectory analysis CLI (--diversity, --mixture)
│ ├── analyze_game.py # Game-specific trajectory analysis
│ ├── generate_report.py # Automated experiment reporting
│ ├── analyze_within_batch.py # Within-batch diversity analysis
│ └── parse_game_logs.py # Convert slurm game logs to trajectory JSONL
│
├── src/unexploitable_search/ # Main Python package
│ ├── config/ # Hydra config dataclasses
│ ├── wandb_utils.py # W&B helpers (sync, artifact upload)
│ ├── settings/ # Task domains
│ │ ├── setting.py # Base Setting class
│ │ ├── quest.py # Base Quest class (+ NoneSideQuest placeholder)
│ │ ├── apps/ # APPS (code generation, sandboxed exec)
│ │ ├── toy/ # Toy (5 Python tasks, diversity experiments)
│ │ │ ├── tasks.py # Task definitions + test cases
│ │ │ ├── properties.py # 20 structural/naming property detectors
│ │ │ ├── metrics.py # Diversity metrics (unique ASTs, Self-BLEU, etc.)
│ │ │ └── quests/ # exec()-based evaluation (no sandbox)
│ │ └── alpaca/ # Alpaca (text generation)
│ ├── train/
│ │ ├── utils.py # Shared training utilities
│ │ ├── trainers/ # Trainer implementations
│ │ │ ├── grpo_trainer.py # Base GRPO
│ │ │ ├── maxent_trainer.py # + entropy bonus
│ │ │ ├── kl_from_base_trainer.py # + KL penalty
│ │ │ └── game_trainer/ # Two-player game variants (+ ibr_controller.py)
│ │ ├── callbacks/ # TrainerCallbacks
│ │ │ ├── trajectory_logger.py # Unified trajectory logging
│ │ │ ├── best_reward.py # Save best-reward checkpoint
│ │ │ ├── highest_accuracy.py # Save highest-accuracy checkpoint
│ │ │ └── clean_sandbox_storage.py
│ │ └── mitigation_strats/ # Training orchestration
│ │ ├── train.py # Main entry: train_with_mitigation_strat()
│ │ └── strats/ # Reward wrappers (length_penalty)
│ ├── eval/ # Evaluation pipeline
│ ├── judges/ # API-based judges (Claude, GPT)
│ ├── logging/ # Completion logger + heuristics
│ └── tests/ # Test suite
│
├── pyproject.toml
├── setup.sh
└── uv.lock
A Setting defines a task domain. Three are available:
| Setting | Description | Main quest (reward) |
|---|---|---|
apps | APPS code-generation | Pass unit tests (sandboxed) |
toy | 5 short Python tasks | Pass test cases (exec-based, no sandbox) |
alpaca | Alpaca instruction-following | High-quality response (LLM-judged) |
The toy setting is a lightweight alternative to APPS for fast iteration and diversity experiments. It defines 5 deterministic Python function tasks (remove duplicates, flatten, char frequency, second largest, run-length encoding), each solvable in 1–6 lines with multiple valid algorithmic approaches. Evaluation uses exec() directly — no sandbox needed. Includes 20 structural/naming properties and diversity metrics (unique ASTs, Self-BLEU, BoN coverage) for measuring output diversity.
Note:
side_questis a vestigial run-name label whose sole valid value isnone; it carries no behaviour.
The model_organism config selects how the starting policy is loaded before RL. Three loaders:
| Type | Config key | Description |
|---|---|---|
prompted | model_organism=prompted | Base/IT model wrapped with a fresh LoRA adapter |
trained | model_organism=trained | A LoRA-adapted checkpoint loaded from a W&B artifact |
trained_hf | model_organism=trained_hf | A checkpoint loaded from the HuggingFace Hub |
Training applies RL (GRPO) to the starting model, rewarding correct task completion while encouraging a semantically diverse set of solutions. Six strategies:
| Strategy | Config | Trainer | Key parameter |
|---|---|---|---|
none | strat=none | GRPOTrainer | — (baseline) |
entropy_bonus | strat=entropy_bonus | MaxEntGRPOTrainer | entropy_coef=0.00352 |
kl_from_base_penalty | strat=kl_from_base_penalty | KLFromBaseGRPOTrainer | kl_from_base_coef=0.00721 |
length_penalty | strat=length_penalty | GRPOTrainer | length_penalty_coefficient=2.131e-6 |
game | strat=game | GRPOGameTrainer | two_player_coefficient=0.226, property_checker_model_id |
hard_game | strat=hard_game | GRPOHardGameTrainer | two_player_coefficient=0.226, property_checker_model_id |
The game / hard_game strategies are the GAP method that is the focus of this repo; the others are diversity baselines.
All trainers extend a custom fork of TRL's GRPO implementation:
Reward functions are composed at runtime:
length_penalty strategyThe TrajectoryLoggerCallback captures per-completion data during training:
Outputs to wandb Tables, local JSONL files, or both. Configured under train.trajectory_logging in the mitigation config.
uv run scripts/train/train_with_mitigation_strat.py \
setting=apps \
side_quest=none \
model_organism=prompted \
it_model=qwen3_8b \
strat=game
Key overrides:
strat=none / entropy_bonus / kl_from_base_penalty / length_penalty / hard_game — choose the strategy (game/hard_game are GAP; the rest are diversity baselines)model_organism=trained model_organism.wandb_artifact_path=<path> — start from a trained checkpoint instead of a fresh LoRAseed=42 — set random seedstrat.trainer_config.max_steps=500 — override training lengthtrain.trajectory_logging.enabled=true — enable trajectory logging (on by default)train.trajectory_logging.backend=local — JSONL only (no wandb tables)Sweep across strategies:
uv run scripts/train/train_with_mitigation_strat.py --multirun \
setting=apps \
side_quest=none \
model_organism=prompted \
it_model=qwen3_8b \
strat=none,entropy_bonus,kl_from_base_penalty,length_penalty,game
Benign evaluation (task-correctness on the held-out test split):
uv run scripts/eval/run_benign_eval.py setting=apps
For semantic-diversity evaluation on the toy setting, see Diversity Eval below.
After a training run with trajectory logging enabled:
python scripts/analyze_trajectories.py outputs/.../trajectories/trajectories_*.jsonl
Options:
--filter 100-300 — restrict to a step range--heuristic num_functions — show trend for a specific structural heuristic--game — show game-specific metrics (game_reward, alice/charlie flagged rates, property diversity)--diversity — compute per-step diversity metrics (unique ASTs, Self-BLEU, BoN coverage; toy setting only)--bon-reference path.jsonl — compute BoN coverage against a reference set--mixture 0.3 — pool completions from the trailing 30% of training to compute mixture-policy diversity (requires --diversity)The project uses Hydra for composable configuration. All configs live under conf/. Values marked ??? are required and must be provided on the command line. Any config value can be overridden from the CLI.
Each entry point has a main config that pulls in config groups via a defaults list:
conf/mitigation_strats/config.yaml <- main config
defaults:
- it_model: ??? <- REQUIRED (8 options)
- adapter: lora <- defaults to LoRA
- model_organism: ??? <- REQUIRED (3 options)
- strat: ??? <- REQUIRED (6 options)
- setting: ??? <- REQUIRED (3 options)
Shared config groups live in conf/_shared/ and are available to all entry points via Hydra's searchpath. Strategy configs (conf/mitigation_strats/strat/) nest further, each selecting a trainer_config sub-group.
Override any value on the CLI with dot-notation:
uv run scripts/train/train_with_mitigation_strat.py \
setting=apps side_quest=none model_organism=prompted \
it_model=qwen3_8b strat=game \
strat.trainer_config.max_steps=200 \
strat.trainer_config.learning_rate=1e-5 \
train.trajectory_logging.backend=local \
seed=123
Hydra's --multirun flag submits a grid of jobs to SLURM (configured in conf/_shared/hydra/launcher/slurm.yaml).
Configs use three interpolation types:
| Syntax | Example | Purpose |
|---|---|---|
${key.subkey} | ${it_model.name} | Reference other config values |
${oc.env:VAR,default} | ${oc.env:PROJECT_SHARED,.} | Environment variable with fallback |
${now:FORMAT} | ${now:%Y-%m-%d} | Timestamp (strftime) |
${hydra:...} | ${hydra:runtime.choices.strat} | Hydra runtime metadata |
Source: conf/mitigation_strats/config.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
side_quest | str | none | Vestigial run-name / experiment-dir label; only none is valid (side objectives were stripped) |
seed | int | 0 | Random seed |
resume | bool | false | Resume from checkpoint (opt-in) |
wandb_mode | str | offline | W&B mode: offline or online |
wandb_tags | list/null | null | Optional W&B tags |
project | str | unexploitable-search | W&B project name |
run_name | str | (auto-generated) | Run name (interpolated from model, strat, seed, timestamp) |
Source: conf/mitigation_strats/strat/trainer_config/base_grpo.yaml. All strategy trainer configs inherit from this.
| Parameter | Type | Default | Description |
|---|---|---|---|
per_device_train_batch_size | int | 2 | Batch size per GPU |
gradient_accumulation_steps | int | 4 | Gradient accumulation (effective batch = 2 x 4 = 8) |
max_steps | int | 1000 | Total training steps |
learning_rate | float | 3e-5 | Learning rate |
max_completion_length | int | 768 | Max generated tokens per completion |
max_prompt_length | int/null | null | Max prompt tokens (null = auto: max_length - max_completion_length) |
num_generations_eval | int | 1 | Completions per prompt during eval |
logging_steps | int | 10 | Log metrics every N steps |
log_completions | bool | true | Log completions to W&B |
do_eval | bool | true | Run evaluation |
eval_on_start | bool | false | Evaluate before training begins |
eval_strategy | str | steps | Eval trigger: steps or epoch |
eval_steps | int | 100 | Evaluate every N steps |
save_strategy | str | steps | Checkpoint trigger |
save_steps | int | 50 | Save checkpoint every N steps |
bf16 | bool | true | Use bfloat16 precision |
lr_scheduler_type | str | constant | Learning rate scheduler type |
torch_empty_cache_steps | int | 4 | Steps between torch.cuda.empty_cache() calls (prevents CUDA allocator RSS leak on GH200) |
report_to | str | wandb | Reporting backend |
output_dir | str | (auto) | Output directory (interpolated with timestamp and run name) |
TRL algorithm defaults (not in config files, but active at runtime via TRL's GRPOConfig):
| Parameter | Default | Description |
|---|---|---|
num_generations | 8 | Completions per prompt per generation cycle (G in the GRPO paper) |
num_iterations | 1 | Policy update steps per generation (μ in the paper; >1 = multi-step GRPO) |
epsilon | 0.2 | PPO-style clipping bound (symmetric; epsilon_high defaults to same) |
beta | 0.0 | KL penalty weight against ref model (0 = no ref model loaded) |
loss_type | dapo | Token-level loss aggregation. Alternatives: grpo, bnpo, dr_grpo |
temperature | 1.0 | Sampling temperature for generation |
scale_rewards | group | Advantage normalization: group (per-prompt), batch, or none |
steps_per_generation | (= grad_accum) | Micro-steps between generation cycles. Defaults to gradient_accumulation_steps |
Override any of these on the CLI, e.g. strat.trainer_config.num_generations=16.
Prompt budget: max_length (4096, set at top level) minus max_completion_length (768) = 3328 tokens available for prompts. Note: 21% of APPS samples with 4 references exceed this budget.
Source: conf/_shared/adapter/lora.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
r | int | 32 | LoRA rank |
lora_alpha | int | 16 | LoRA scaling factor |
lora_dropout | float | 0.05 | Dropout on LoRA layers |
target_modules | list | [q_proj, k_proj, v_proj, o_proj] | Attention projection layers to adapt |
Each strategy selects a trainer config (inheriting from base_grpo) and may add its own parameters. Source: conf/mitigation_strats/strat/.
strat=none — Vanilla GRPO (baseline)No additional parameters. Uses trainer_config: vanilla_grpo.
strat=game — Two-Player Game| Parameter | Type | Default | Description |
|---|---|---|---|
two_player_coefficient | float | 0.22599 | Weight for adversary vs proposer loss |
num_references | int | 4 | Reference solutions for scoring |
reward_shaping_weight | float | 0.0 | Weight of +/- nuggets in _get_game_rewards() |
game_score_weights | list/null | null | Per-reward-function weights (null = uniform). Length must match number of reward functions (excluding game reward) |
preprocess_reference_solutions | bool | true | Pre-process reference solutions |
train_adversary | bool | true | Whether to train the adversary (Charlie) |
alice_turns_per_cycle | int | 1 | Generation cycles per phase where Alice trains (adversary frozen) |
adversary_turns_per_cycle | int | 3 | Generation cycles per phase where adversary trains (Alice frozen). 0 = simultaneous (no alternation) |
property_checker_model_id | str/null | null | HF model ID for a separate property checker model (e.g. Qwen/Qwen3-8B). When null, uses a frozen LoRA adapter on the training model |
strat=hard_game — Three-Agent GameSame parameters as game, including property_checker_model_id (adds a separate Bob agent in Python code; config is identical). Also supports alice_turns_per_cycle and adversary_turns_per_cycle for frozen best-response alternation.
strat=entropy_bonus — Entropy Bonus| Parameter | Type | Default | Description |
|---|---|---|---|
entropy_coef | float | 0.00352 | Coefficient for entropy bonus in loss (set in trainer_config) |
strat=kl_from_base_penalty — KL-from-Base Penalty| Parameter | Type | Default | Description |
|---|---|---|---|
kl_from_base_coef | float | 0.00721 | KL divergence penalty coefficient (set in both strategy and trainer config) |
base_model_hf_path | str | (from it_model) | HF path for the base (pre-IT) model, auto-resolved from it_model.base_hf_path |
strat=length_penalty — Length Penalty| Parameter | Type | Default | Description |
|---|---|---|---|
length_penalty_coefficient | float | 2.131e-6 | Per-token length penalty |
Source: conf/mitigation_strats/config.yaml under train.sandbox
| Parameter | Type | Default | Description |
|---|---|---|---|
train.sandbox.type | str | podman | Sandbox backend: docker, podman, or inspect |
train.sandbox.pool_size | int | 8 | Number of concurrent sandbox containers |
train.max_length | int | 4096 | Total token budget (prompt + completion) |
train.max_validation_set_size | int | 100 | Cap on validation set size |
Source: conf/mitigation_strats/config.yaml under train.trajectory_logging
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable/disable trajectory logging |
backend | str | both | Where to log: wandb, local (JSONL), or both |
local_output_dir | str/null | null | JSONL output dir (null = {output_dir}/trajectories) |
log_every_n_steps | int | 1 | Log every N logging steps |
max_samples_per_step | int/null | null | Samples to log per step (null = all) |
print_samples | int | 5 | Samples for rich console output |
Source: conf/evals/_shared/inspect/inspect.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
epochs | int | 1 | Evaluation epochs |
limit | int | 1000 | Max samples to evaluate |
max_samples | int | 4 | Batch size per epoch |
temperature | float | 1.0 | Sampling temperature |
sandbox | str | podman | Sandbox type |
sandbox_pool_size | int | 4 | Concurrent sandbox containers |
Configured under conf/_shared/it_model/:
| Config key | Instruction-Tuned Model | Base (Pre-IT) Model |
|---|---|---|
qwen2.5_0.5b | Qwen/Qwen2.5-0.5B-Instruct | Qwen/Qwen2.5-0.5B |
qwen2.5_32b | Qwen/Qwen2.5-Coder-32B-Instruct | Qwen/Qwen2.5-Coder-32B |
qwen3_1.7b | Qwen/Qwen3-1.7B | Qwen/Qwen3-1.7B-Base |
qwen3_1.7b_base | Qwen/Qwen3-1.7B-Base | Qwen/Qwen3-1.7B-Base |
qwen3_8b | Qwen/Qwen3-8B | Qwen/Qwen3-8B-Base |
qwen3_8b_base | Qwen/Qwen3-8B-Base | Qwen/Qwen3-8B-Base |
qwen3_14b | Qwen/Qwen3-14B | Qwen/Qwen3-14B-Base |
llama3_8b | meta-llama/Llama-3.1-8B-Instruct | meta-llama/Llama-3.1-8B |
gemma3_4b | google/gemma-3-4b-it | google/gemma-3-4b-pt |
gemma3_4b_base | google/gemma-3-4b-pt | google/gemma-3-4b-pt |
gemma3_12b | google/gemma-3-12b-it | google/gemma-3-12b-pt |
gemma3_27b | google/gemma-3-27b-it | google/gemma-3-27b-pt |
The base model path is used by kl_from_base_penalty strategy to compute KL divergence.
Source: conf/_shared/hydra/launcher/slurm.yaml. Used when --multirun is passed.
| Parameter | Type | Default | Description |
|---|---|---|---|
gpus_per_node | int | 1 | GPUs per SLURM job |
timeout_min | int | 1440 | Job timeout (24 hours) |
max_num_timeout | int | 6 | Max timeout retries |
mem_gb | int | 80 | Memory per job (GB) |
The launcher also sets up cache directories (HF_HOME, TRANSFORMERS_CACHE, WANDB_DIR, UV_CACHE_DIR) under $PROJECT_SHARED/.cache/.
# All non-GPU, non-sandbox tests
uv run pytest src/unexploitable_search/tests/ -v -m "not gpu and not sandbox and not slow"
# GPU tests
uv run pytest src/unexploitable_search/tests/ -v -m gpu
# Sandbox tests (requires podman)
uv run pytest src/unexploitable_search/tests/ -v -m sandbox
Test markers: sandbox (requires podman), gpu (requires GPU), slow (multi-step training).
On the cluster, use srun — see CLAUDE.md for srun patterns.
Tests are grouped into 13 categories. The E2E Plan column references the category number for each test.
| File | E2E Plan | What it tests |
|---|---|---|
test_mitigation_strategies.py | 8c | Game reward outcomes (Bob/Alice wins, trivial scenarios), reward shaping with nuggets, eps-lexicographic scaling, entropy computation, GRPO group advantage structure |
test_merge_pipeline.py | — | Merge-and-reload pipeline is numerically lossless; no NaN/Inf in merged output |
test_prompt_length_budget.py | — | Game prompts fit within the 3328-token budget (documents 21% violation with 4 references) |
test_completion_logger.py | — | Heuristic functions (syntax validity, tmp-variable, env-check, stderr-write, list comprehension, etc.) and JSONL roundtrip |
test_config.py | — | Side-quest registry placeholder (none-only), model config fields, strategy config consistency |
test_data_pipeline.py | — | Dataset splits (non-overlapping, deterministic), code extraction (markdown fences, preamble, indentation), tokenizer EOS/pad, reward aggregation |
test_trajectory_logger.py | — | TrajectoryLoggerCallback: JSONL writes, heuristic enrichment, game trace, strategy metrics, sampling controls, wandb integration (mocked) |
test_game_trajectory.py | 9 | Game best-case trajectory verification: mixed group rewards/advantages, coefficient scaling, degenerate groups, asymmetric splits, shaping, multi-property, multi-prompt batches, edge cases |
test_game_analysis.py | — | Branch classification logic in analyze_game.py, game metrics logging in game trainers (branch fractions, phase tracking) |
test_length_penalty.py | — | Length penalty reward function: linear scaling, coefficient sensitivity, empty/whitespace handling, interaction with tokenizer |
test_alternation_phase.py | — | Frozen best-response alternation phase cycling, simultaneous mode, adapter switching |
test_fake_sandbox.py | 11 | FakeSandbox for config-to-reward testing: always-pass/fail, partial scores, string matching, integration with make_quest_grpo_reward |
test_strategy_ablation.py | 12 | All strategy coefficients at zero produce zero contribution (length, game, shaping, entropy, KL), individual and combined |
test_eval_train_consistency.py | 13 | Eval/train mode flags set correctly, game logic checks training flag, reward logging guards, rewards_per_func shape consistency |
test_sandbox_failures_propagate.py | 8 | Sandbox errors propagate (creation failure raises, stderr in error, timeout returns error); documents current silent-failure bug |
@pytest.mark.gpu)| File | E2E Plan | What it tests | Markers |
|---|---|---|---|
test_training_eval_consistency.py | 3 | KL-from-base is zero for identical models, positive after perturbation, penalty sign in loss; entropy bonus reduces loss; dtype consistency (bfloat16) | gpu |
test_gradient_direction.py | 2, 3 | Single-step gradient direction; multi-step (5-step) token-level probability tracking; reward function consistency | gpu |
test_adapter_isolation.py | 5, 6 | Only LoRA weights change during training; changes are nonzero, no NaN, reasonable magnitudes; training one adapter doesn't affect others; property_checker always frozen; charlie updates when train_adversary=True | gpu |
test_behavioral_regression.py | 1, 10 | Completions shorten after short-reward training; tmp usage decreases after no-tmp reward; constant reward produces no dramatic shift; length penalty shortens completions | gpu, slow |
test_lora_logprobs_change.py | — | PEFT has trainable params; logprobs shift after training; save/reload preserves logprobs; eval load path matches | gpu, slow |
test_trainer_smoke.py | — | Each custom trainer (game, maxent, KL-from-base) completes 2 training steps without crashing | gpu, slow |
test_property_checker.py | 8b | Batched vs sequential property checking equivalence, edge cases, model competency (True/False format compliance, positive/negative detection) | gpu |
temp/test_pr1_validation.py | — | Temporary (delete after PR1). Qwen3-8B / Qwen2.5-7B VRAM checks, forward pass, LoRA application | gpu, slow |
@pytest.mark.sandbox)| File | E2E Plan | What it tests |
|---|---|---|
test_sandbox_stress.py | 3, 8 | 100 concurrent exec calls succeed with no output mixing; non-zero exit codes propagate; timeouts don't silently succeed |
test_podman_storage_fix.py | — | Verifies /tmp-based podman-hpc storage override works on nodes where /local/user/ is not provisioned |
| File | Purpose |
|---|---|
test_sandbox_clean_storage.py | Manual PooledSandbox.clean_storage() debugging |
test_orphaned_processes.py | Orphaned-process detection after timeout (3 tests, all xfail — sandbox genuinely leaves orphans) |
The toy setting is designed for fast diversity experiments comparing training strategies.
Compares 7 training arms on Qwen2.5-0.5B-Instruct across 5 simple Python tasks, measuring whether GAP (game-based adversarial training) produces more diverse solutions than entropy/KL/length baselines.
Arms: vanilla GRPO, entropy bonus (0.01/0.05/0.1), length penalty, KL-from-base, GAP (game)
bash scripts/experiments/toy_0.6b/run_all.sh
Same arms on Qwen3-1.7B. Includes a relaunch variant using Qwen3-8B as the property checker judge.
# Full batch (baseline + 7 arms)
bash scripts/experiments/toy_1.7b/run_all.sh
# GAP with 8B property judge (simultaneous + 3:1 alternation)
bash scripts/experiments/toy_1.7b/relaunch_gap_8b_judge.sh
uv run python scripts/eval/run_toy_diversity_eval.py \
--base_model Qwen/Qwen3-1.7B \
--adapter_path <output_dir>/final_model \
--ref_freqs <baseline_dir>/ref_freqs.json \
--num_completions 100
Outputs (on /projects/):
experiments/toy_0.6b/
├── env.sh # Shared environment (TRANSFORMERS_CACHE, etc.)
├── ref_freqs.json # Baseline property frequencies
├── logs/ # Slurm .out/.err per job
└── outputs/
├── vanilla_s0/ # Each arm has:
│ ├── checkpoint-50/ ... checkpoint-500/
│ ├── best_model/
│ └── final_model/
│ ├── adapter_model.safetensors
│ └── adapter_config.json
├── entropy_0.01_s0/
├── entropy_0.05_s0/
├── entropy_0.1_s0/
├── length_s0/
├── kl_s0/
└── gap_s0/ # Also contains charlie/ and property_checker/ subdirs
Full-scale diversity benchmark on Qwen3-8B-Base with APPS code generation, comparing GAP (game) against the vanilla / entropy / KL / length baselines across seeds.
bash scripts/experiments/qwen3_8b_base/run_diversity_benchmark.sh
Experiment outputs follow a naming convention that enables auto-discovery:
experiments/qwen3_8b_base/
├── outputs/
│ ├── game_none_s0/ # {strat}_{side_quest}_s{seed}; side_quest is always `none`
│ │ ├── checkpoint-50/
│ │ ├── checkpoint-100/
│ │ ├── final_model/ # Present when training completes
│ │ └── trajectories/
│ │ ├── trajectories_20260215_1234.jsonl
│ │ └── trajectories_latest.jsonl -> trajectories_20260215_1234.jsonl
│ ├── game_none_s1/
│ ├── entropy_bonus_none_s0/
│ └── ...
├── manifest.json # Launch manifest (job IDs, arms, output dirs)
└── logs/ # Slurm .out/.err files
{strat}_{side_quest}_s{seed} — side_quest is now always none (e.g. game_none_s0). The discovery parser still recognises the old side-quest names so historical experiment dirs keep parsing.{arm_name}_s{seed} (e.g. gap_8b_eps_sim_s0)trajectories_latest.jsonl: Symlink to the most recent trajectory file (auto-created)manifest.json: Written by launch scripts, records Slurm job IDs per armCheck on all arms in an experiment:
python scripts/experiment_status.py /path/to/experiment
python scripts/experiment_status.py /path/to/experiment --side-quest none
python scripts/experiment_status.py /path/to/experiment --strat game
Shows DONE/RUN/MISS status, checkpoint progress, and trajectory line counts for each arm.
# Direct file paths
python scripts/analyze_trajectories.py outputs/.../trajectories/*.jsonl
python scripts/analyze_trajectories.py --game --filter 100-300 outputs/.../trajectories/*.jsonl
# Auto-discover from experiment directory
python scripts/analyze_trajectories.py --experiment-dir /path/to/experiment --strat game
python scripts/analyze_trajectories.py --experiment-dir /path/to/experiment --side-quest none --heuristic num_functions
# Direct file paths
python scripts/analyze_game.py outputs/.../trajectories/*.jsonl
# Auto-discover from experiment directory
python scripts/analyze_game.py --experiment-dir /path/to/experiment --strat game --side-quest none
# From slurm log files (when trajectory logging is disabled)
python scripts/parse_game_logs.py <logfile>.out -o traj.jsonl
python scripts/analyze_game.py traj.jsonl
Analysis modes: --summary (default), --branches, --properties, --traces N, --plot, --heuristics, --phase-split.
This repository is released under the Apache License 2.0. See
NOTICE for required attribution.
The trainer files under src/unexploitable_search/train/trainers/ are copies of
TRL's GRPOTrainer (Copyright The HuggingFace
Team, Apache 2.0), modified for this project. Each carries an Apache header noting the
modifications.
The Apache 2.0 license covers this repository's code only. Datasets and model weights are downloaded at runtime, are not redistributed here, and remain governed by their own licenses:
| Asset | Used for | License |
|---|---|---|
codeparrot/apps (Hendrycks et al.) | APPS code-generation domain | MIT |
tatsu-lab/alpaca | Alpaca instruction-following domain | CC BY-NC 4.0 (non-commercial); derived from OpenAI text-davinci-003 outputs and subject to OpenAI's terms |
Qwen/Qwen3-* weights | Base / instruct models | Apache 2.0 |
google/gemma-3-*-it weights | Instruct / property-checker judge models | Gemma Terms of Use |
Note in particular that the Alpaca data is non-commercial: the Alpaca domain is suitable for academic / non-commercial research only, regardless of this repository's code license. Users are responsible for complying with each asset's license.
If you use this code, please cite the paper:
@misc{garl_without_demonstrations,
title = {GARL without demonstrations},
author = {Jacob Pfau and Arathi Mani and Alejandro Aristizabal and Stefan Jones and Xanthe Spence and Jazon Szabo},
year = {2026},
note = {https://github.com/AI-Safety-Institute/unexploitable-search}
}
Python
91.9%
Shell
8.1%
Codebase for experiments on unexploitable search. This repo studies a new generative adversarial RL method which works without demonstrations. The goal is to enforce diversity at a semantic level.
The core method is a two-player game (GAP — Game-based Adversarial Properties): a proposer (Alice) solves the task while an adversary (Charlie) identifies non-essential properties of Alice's solutions and proposes alternatives without them. Alice is rewarded for solving the task in ways the adversary cannot pin down to a single exploitable pattern — pushing the policy toward a semantically diverse set of solutions. The game needs no human demonstrations of "diverse" behaviour: the adversary generates the contrast set on the fly.
bash setup.sh # installs uv, runs uv sync
Requires:
podman-hpc (or Docker) for sandboxed code execution (APPS domain)Environment variables:
PROJECT_SHARED — root for multirun/output dirs (defaults to .)WANDB_MODE — offline (default) or onlinePYTHON_TMPDIR — temp directory for model downloadsunexploitable-search/
├── conf/ # Hydra configs (YAML)
│ ├── _shared/ # Shared config fragments
│ │ ├── adapter/lora.yaml # LoRA: r=32, alpha=16
│ │ ├── it_model/ # Base models (qwen3_8b, llama3_8b, gemma3_12b, …)
│ │ ├── model_organism/ # Starting-model loaders (prompted, trained, trained_hf)
│ │ └── setting/ # Task domains (apps, alpaca, toy)
│ ├── mitigation_strats/ # RL training config
│ │ ├── config.yaml # Main config
│ │ └── strat/ # Strategy selection (none, game, hard_game,
│ │ │ # entropy_bonus, kl_from_base_penalty, length_penalty)
│ │ └── trainer_config/ # GRPO trainer hyperparams
│ └── evals/ # Evaluation configs (benign task-correctness)
│
├── scripts/ # User-facing entry points
│ ├── train/
│ │ ├── train_with_mitigation_strat.py
│ │ └── gpu_queue_runner.py # Multi-GPU job queue
│ ├── eval/
│ │ ├── run_benign_eval.py # Task-correctness eval
│ │ ├── run_toy_baseline.py # Toy: baseline characterisation
│ │ ├── run_toy_diversity_eval.py # Toy: diversity eval on checkpoints
│ │ └── generate_bon_reference.py # BoN reference generation (toy)
│ ├── experiments/
│ │ ├── toy_0.6b/ # Toy 0.6B experiment batch scripts
│ │ ├── toy_1.7b/ # Toy 1.7B experiment (Qwen3-1.7B)
│ │ └── qwen3_8b_base/ # Q8B-Base diversity benchmark
│ ├── slurm/ # Slurm job helpers
│ │ ├── auto_retry.sh # Failure-mode-aware retry with checkpoint resume
│ │ └── podman_storage.sh # Fix podman-hpc storage on unreliable nodes
│ ├── lib/ # Shared analysis utilities
│ │ └── experiment_discovery.py # Auto-discover experiment arms & trajectories
│ ├── debug/ # Profiling & competence checks
│ ├── plots/ # Plotting utilities
│ ├── experiment_status.py # Show experiment arm status (DONE/RUN/MISS)
│ ├── analyze_trajectories.py # JSONL trajectory analysis CLI (--diversity, --mixture)
│ ├── analyze_game.py # Game-specific trajectory analysis
│ ├── generate_report.py # Automated experiment reporting
│ ├── analyze_within_batch.py # Within-batch diversity analysis
│ └── parse_game_logs.py # Convert slurm game logs to trajectory JSONL
│
├── src/unexploitable_search/ # Main Python package
│ ├── config/ # Hydra config dataclasses
│ ├── wandb_utils.py # W&B helpers (sync, artifact upload)
│ ├── settings/ # Task domains
│ │ ├── setting.py # Base Setting class
│ │ ├── quest.py # Base Quest class (+ NoneSideQuest placeholder)
│ │ ├── apps/ # APPS (code generation, sandboxed exec)
│ │ ├── toy/ # Toy (5 Python tasks, diversity experiments)
│ │ │ ├── tasks.py # Task definitions + test cases
│ │ │ ├── properties.py # 20 structural/naming property detectors
│ │ │ ├── metrics.py # Diversity metrics (unique ASTs, Self-BLEU, etc.)
│ │ │ └── quests/ # exec()-based evaluation (no sandbox)
│ │ └── alpaca/ # Alpaca (text generation)
│ ├── train/
│ │ ├── utils.py # Shared training utilities
│ │ ├── trainers/ # Trainer implementations
│ │ │ ├── grpo_trainer.py # Base GRPO
│ │ │ ├── maxent_trainer.py # + entropy bonus
│ │ │ ├── kl_from_base_trainer.py # + KL penalty
│ │ │ └── game_trainer/ # Two-player game variants (+ ibr_controller.py)
│ │ ├── callbacks/ # TrainerCallbacks
│ │ │ ├── trajectory_logger.py # Unified trajectory logging
│ │ │ ├── best_reward.py # Save best-reward checkpoint
│ │ │ ├── highest_accuracy.py # Save highest-accuracy checkpoint
│ │ │ └── clean_sandbox_storage.py
│ │ └── mitigation_strats/ # Training orchestration
│ │ ├── train.py # Main entry: train_with_mitigation_strat()
│ │ └── strats/ # Reward wrappers (length_penalty)
│ ├── eval/ # Evaluation pipeline
│ ├── judges/ # API-based judges (Claude, GPT)
│ ├── logging/ # Completion logger + heuristics
│ └── tests/ # Test suite
│
├── pyproject.toml
├── setup.sh
└── uv.lock
A Setting defines a task domain. Three are available:
| Setting | Description | Main quest (reward) |
|---|---|---|
apps | APPS code-generation | Pass unit tests (sandboxed) |
toy | 5 short Python tasks | Pass test cases (exec-based, no sandbox) |
alpaca | Alpaca instruction-following | High-quality response (LLM-judged) |
The toy setting is a lightweight alternative to APPS for fast iteration and diversity experiments. It defines 5 deterministic Python function tasks (remove duplicates, flatten, char frequency, second largest, run-length encoding), each solvable in 1–6 lines with multiple valid algorithmic approaches. Evaluation uses exec() directly — no sandbox needed. Includes 20 structural/naming properties and diversity metrics (unique ASTs, Self-BLEU, BoN coverage) for measuring output diversity.
Note:
side_questis a vestigial run-name label whose sole valid value isnone; it carries no behaviour.
The model_organism config selects how the starting policy is loaded before RL. Three loaders:
| Type | Config key | Description |
|---|---|---|
prompted | model_organism=prompted | Base/IT model wrapped with a fresh LoRA adapter |
trained | model_organism=trained | A LoRA-adapted checkpoint loaded from a W&B artifact |
trained_hf | model_organism=trained_hf | A checkpoint loaded from the HuggingFace Hub |
Training applies RL (GRPO) to the starting model, rewarding correct task completion while encouraging a semantically diverse set of solutions. Six strategies:
| Strategy | Config | Trainer | Key parameter |
|---|---|---|---|
none | strat=none | GRPOTrainer | — (baseline) |
entropy_bonus | strat=entropy_bonus | MaxEntGRPOTrainer | entropy_coef=0.00352 |
kl_from_base_penalty | strat=kl_from_base_penalty | KLFromBaseGRPOTrainer | kl_from_base_coef=0.00721 |
length_penalty | strat=length_penalty | GRPOTrainer | length_penalty_coefficient=2.131e-6 |
game | strat=game | GRPOGameTrainer | two_player_coefficient=0.226, property_checker_model_id |
hard_game | strat=hard_game | GRPOHardGameTrainer | two_player_coefficient=0.226, property_checker_model_id |
The game / hard_game strategies are the GAP method that is the focus of this repo; the others are diversity baselines.
All trainers extend a custom fork of TRL's GRPO implementation:
Reward functions are composed at runtime:
length_penalty strategyThe TrajectoryLoggerCallback captures per-completion data during training:
Outputs to wandb Tables, local JSONL files, or both. Configured under train.trajectory_logging in the mitigation config.
uv run scripts/train/train_with_mitigation_strat.py \
setting=apps \
side_quest=none \
model_organism=prompted \
it_model=qwen3_8b \
strat=game
Key overrides:
strat=none / entropy_bonus / kl_from_base_penalty / length_penalty / hard_game — choose the strategy (game/hard_game are GAP; the rest are diversity baselines)model_organism=trained model_organism.wandb_artifact_path=<path> — start from a trained checkpoint instead of a fresh LoRAseed=42 — set random seedstrat.trainer_config.max_steps=500 — override training lengthtrain.trajectory_logging.enabled=true — enable trajectory logging (on by default)train.trajectory_logging.backend=local — JSONL only (no wandb tables)Sweep across strategies:
uv run scripts/train/train_with_mitigation_strat.py --multirun \
setting=apps \
side_quest=none \
model_organism=prompted \
it_model=qwen3_8b \
strat=none,entropy_bonus,kl_from_base_penalty,length_penalty,game
Benign evaluation (task-correctness on the held-out test split):
uv run scripts/eval/run_benign_eval.py setting=apps
For semantic-diversity evaluation on the toy setting, see Diversity Eval below.
After a training run with trajectory logging enabled:
python scripts/analyze_trajectories.py outputs/.../trajectories/trajectories_*.jsonl
Options:
--filter 100-300 — restrict to a step range--heuristic num_functions — show trend for a specific structural heuristic--game — show game-specific metrics (game_reward, alice/charlie flagged rates, property diversity)--diversity — compute per-step diversity metrics (unique ASTs, Self-BLEU, BoN coverage; toy setting only)--bon-reference path.jsonl — compute BoN coverage against a reference set--mixture 0.3 — pool completions from the trailing 30% of training to compute mixture-policy diversity (requires --diversity)The project uses Hydra for composable configuration. All configs live under conf/. Values marked ??? are required and must be provided on the command line. Any config value can be overridden from the CLI.
Each entry point has a main config that pulls in config groups via a defaults list:
conf/mitigation_strats/config.yaml <- main config
defaults:
- it_model: ??? <- REQUIRED (8 options)
- adapter: lora <- defaults to LoRA
- model_organism: ??? <- REQUIRED (3 options)
- strat: ??? <- REQUIRED (6 options)
- setting: ??? <- REQUIRED (3 options)
Shared config groups live in conf/_shared/ and are available to all entry points via Hydra's searchpath. Strategy configs (conf/mitigation_strats/strat/) nest further, each selecting a trainer_config sub-group.
Override any value on the CLI with dot-notation:
uv run scripts/train/train_with_mitigation_strat.py \
setting=apps side_quest=none model_organism=prompted \
it_model=qwen3_8b strat=game \
strat.trainer_config.max_steps=200 \
strat.trainer_config.learning_rate=1e-5 \
train.trajectory_logging.backend=local \
seed=123
Hydra's --multirun flag submits a grid of jobs to SLURM (configured in conf/_shared/hydra/launcher/slurm.yaml).
Configs use three interpolation types:
| Syntax | Example | Purpose |
|---|---|---|
${key.subkey} | ${it_model.name} | Reference other config values |
${oc.env:VAR,default} | ${oc.env:PROJECT_SHARED,.} | Environment variable with fallback |
${now:FORMAT} | ${now:%Y-%m-%d} | Timestamp (strftime) |
${hydra:...} | ${hydra:runtime.choices.strat} | Hydra runtime metadata |
Source: conf/mitigation_strats/config.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
side_quest | str | none | Vestigial run-name / experiment-dir label; only none is valid (side objectives were stripped) |
seed | int | 0 | Random seed |
resume | bool | false | Resume from checkpoint (opt-in) |
wandb_mode | str | offline | W&B mode: offline or online |
wandb_tags | list/null | null | Optional W&B tags |
project | str | unexploitable-search | W&B project name |
run_name | str | (auto-generated) | Run name (interpolated from model, strat, seed, timestamp) |
Source: conf/mitigation_strats/strat/trainer_config/base_grpo.yaml. All strategy trainer configs inherit from this.
| Parameter | Type | Default | Description |
|---|---|---|---|
per_device_train_batch_size | int | 2 | Batch size per GPU |
gradient_accumulation_steps | int | 4 | Gradient accumulation (effective batch = 2 x 4 = 8) |
max_steps | int | 1000 | Total training steps |
learning_rate | float | 3e-5 | Learning rate |
max_completion_length | int | 768 | Max generated tokens per completion |
max_prompt_length | int/null | null | Max prompt tokens (null = auto: max_length - max_completion_length) |
num_generations_eval | int | 1 | Completions per prompt during eval |
logging_steps | int | 10 | Log metrics every N steps |
log_completions | bool | true | Log completions to W&B |
do_eval | bool | true | Run evaluation |
eval_on_start | bool | false | Evaluate before training begins |
eval_strategy | str | steps | Eval trigger: steps or epoch |
eval_steps | int | 100 | Evaluate every N steps |
save_strategy | str | steps | Checkpoint trigger |
save_steps | int | 50 | Save checkpoint every N steps |
bf16 | bool | true | Use bfloat16 precision |
lr_scheduler_type | str | constant | Learning rate scheduler type |
torch_empty_cache_steps | int | 4 | Steps between torch.cuda.empty_cache() calls (prevents CUDA allocator RSS leak on GH200) |
report_to | str | wandb | Reporting backend |
output_dir | str | (auto) | Output directory (interpolated with timestamp and run name) |
TRL algorithm defaults (not in config files, but active at runtime via TRL's GRPOConfig):
| Parameter | Default | Description |
|---|---|---|
num_generations | 8 | Completions per prompt per generation cycle (G in the GRPO paper) |
num_iterations | 1 | Policy update steps per generation (μ in the paper; >1 = multi-step GRPO) |
epsilon | 0.2 | PPO-style clipping bound (symmetric; epsilon_high defaults to same) |
beta | 0.0 | KL penalty weight against ref model (0 = no ref model loaded) |
loss_type | dapo | Token-level loss aggregation. Alternatives: grpo, bnpo, dr_grpo |
temperature | 1.0 | Sampling temperature for generation |
scale_rewards | group | Advantage normalization: group (per-prompt), batch, or none |
steps_per_generation | (= grad_accum) | Micro-steps between generation cycles. Defaults to gradient_accumulation_steps |
Override any of these on the CLI, e.g. strat.trainer_config.num_generations=16.
Prompt budget: max_length (4096, set at top level) minus max_completion_length (768) = 3328 tokens available for prompts. Note: 21% of APPS samples with 4 references exceed this budget.
Source: conf/_shared/adapter/lora.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
r | int | 32 | LoRA rank |
lora_alpha | int | 16 | LoRA scaling factor |
lora_dropout | float | 0.05 | Dropout on LoRA layers |
target_modules | list | [q_proj, k_proj, v_proj, o_proj] | Attention projection layers to adapt |
Each strategy selects a trainer config (inheriting from base_grpo) and may add its own parameters. Source: conf/mitigation_strats/strat/.
strat=none — Vanilla GRPO (baseline)No additional parameters. Uses trainer_config: vanilla_grpo.
strat=game — Two-Player Game| Parameter | Type | Default | Description |
|---|---|---|---|
two_player_coefficient | float | 0.22599 | Weight for adversary vs proposer loss |
num_references | int | 4 | Reference solutions for scoring |
reward_shaping_weight | float | 0.0 | Weight of +/- nuggets in _get_game_rewards() |
game_score_weights | list/null | null | Per-reward-function weights (null = uniform). Length must match number of reward functions (excluding game reward) |
preprocess_reference_solutions | bool | true | Pre-process reference solutions |
train_adversary | bool | true | Whether to train the adversary (Charlie) |
alice_turns_per_cycle | int | 1 | Generation cycles per phase where Alice trains (adversary frozen) |
adversary_turns_per_cycle | int | 3 | Generation cycles per phase where adversary trains (Alice frozen). 0 = simultaneous (no alternation) |
property_checker_model_id | str/null | null | HF model ID for a separate property checker model (e.g. Qwen/Qwen3-8B). When null, uses a frozen LoRA adapter on the training model |
strat=hard_game — Three-Agent GameSame parameters as game, including property_checker_model_id (adds a separate Bob agent in Python code; config is identical). Also supports alice_turns_per_cycle and adversary_turns_per_cycle for frozen best-response alternation.
strat=entropy_bonus — Entropy Bonus| Parameter | Type | Default | Description |
|---|---|---|---|
entropy_coef | float | 0.00352 | Coefficient for entropy bonus in loss (set in trainer_config) |
strat=kl_from_base_penalty — KL-from-Base Penalty| Parameter | Type | Default | Description |
|---|---|---|---|
kl_from_base_coef | float | 0.00721 | KL divergence penalty coefficient (set in both strategy and trainer config) |
base_model_hf_path | str | (from it_model) | HF path for the base (pre-IT) model, auto-resolved from it_model.base_hf_path |
strat=length_penalty — Length Penalty| Parameter | Type | Default | Description |
|---|---|---|---|
length_penalty_coefficient | float | 2.131e-6 | Per-token length penalty |
Source: conf/mitigation_strats/config.yaml under train.sandbox
| Parameter | Type | Default | Description |
|---|---|---|---|
train.sandbox.type | str | podman | Sandbox backend: docker, podman, or inspect |
train.sandbox.pool_size | int | 8 | Number of concurrent sandbox containers |
train.max_length | int | 4096 | Total token budget (prompt + completion) |
train.max_validation_set_size | int | 100 | Cap on validation set size |
Source: conf/mitigation_strats/config.yaml under train.trajectory_logging
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable/disable trajectory logging |
backend | str | both | Where to log: wandb, local (JSONL), or both |
local_output_dir | str/null | null | JSONL output dir (null = {output_dir}/trajectories) |
log_every_n_steps | int | 1 | Log every N logging steps |
max_samples_per_step | int/null | null | Samples to log per step (null = all) |
print_samples | int | 5 | Samples for rich console output |
Source: conf/evals/_shared/inspect/inspect.yaml
| Parameter | Type | Default | Description |
|---|---|---|---|
epochs | int | 1 | Evaluation epochs |
limit | int | 1000 | Max samples to evaluate |
max_samples | int | 4 | Batch size per epoch |
temperature | float | 1.0 | Sampling temperature |
sandbox | str | podman | Sandbox type |
sandbox_pool_size | int | 4 | Concurrent sandbox containers |
Configured under conf/_shared/it_model/:
| Config key | Instruction-Tuned Model | Base (Pre-IT) Model |
|---|---|---|
qwen2.5_0.5b | Qwen/Qwen2.5-0.5B-Instruct | Qwen/Qwen2.5-0.5B |
qwen2.5_32b | Qwen/Qwen2.5-Coder-32B-Instruct | Qwen/Qwen2.5-Coder-32B |
qwen3_1.7b | Qwen/Qwen3-1.7B | Qwen/Qwen3-1.7B-Base |
qwen3_1.7b_base | Qwen/Qwen3-1.7B-Base | Qwen/Qwen3-1.7B-Base |
qwen3_8b | Qwen/Qwen3-8B | Qwen/Qwen3-8B-Base |
qwen3_8b_base | Qwen/Qwen3-8B-Base | Qwen/Qwen3-8B-Base |
qwen3_14b | Qwen/Qwen3-14B | Qwen/Qwen3-14B-Base |
llama3_8b | meta-llama/Llama-3.1-8B-Instruct | meta-llama/Llama-3.1-8B |
gemma3_4b | google/gemma-3-4b-it | google/gemma-3-4b-pt |
gemma3_4b_base | google/gemma-3-4b-pt | google/gemma-3-4b-pt |
gemma3_12b | google/gemma-3-12b-it | google/gemma-3-12b-pt |
gemma3_27b | google/gemma-3-27b-it | google/gemma-3-27b-pt |
The base model path is used by kl_from_base_penalty strategy to compute KL divergence.
Source: conf/_shared/hydra/launcher/slurm.yaml. Used when --multirun is passed.
| Parameter | Type | Default | Description |
|---|---|---|---|
gpus_per_node | int | 1 | GPUs per SLURM job |
timeout_min | int | 1440 | Job timeout (24 hours) |
max_num_timeout | int | 6 | Max timeout retries |
mem_gb | int | 80 | Memory per job (GB) |
The launcher also sets up cache directories (HF_HOME, TRANSFORMERS_CACHE, WANDB_DIR, UV_CACHE_DIR) under $PROJECT_SHARED/.cache/.
# All non-GPU, non-sandbox tests
uv run pytest src/unexploitable_search/tests/ -v -m "not gpu and not sandbox and not slow"
# GPU tests
uv run pytest src/unexploitable_search/tests/ -v -m gpu
# Sandbox tests (requires podman)
uv run pytest src/unexploitable_search/tests/ -v -m sandbox
Test markers: sandbox (requires podman), gpu (requires GPU), slow (multi-step training).
On the cluster, use srun — see CLAUDE.md for srun patterns.
Tests are grouped into 13 categories. The E2E Plan column references the category number for each test.
| File | E2E Plan | What it tests |
|---|---|---|
test_mitigation_strategies.py | 8c | Game reward outcomes (Bob/Alice wins, trivial scenarios), reward shaping with nuggets, eps-lexicographic scaling, entropy computation, GRPO group advantage structure |
test_merge_pipeline.py | — | Merge-and-reload pipeline is numerically lossless; no NaN/Inf in merged output |
test_prompt_length_budget.py | — | Game prompts fit within the 3328-token budget (documents 21% violation with 4 references) |
test_completion_logger.py | — | Heuristic functions (syntax validity, tmp-variable, env-check, stderr-write, list comprehension, etc.) and JSONL roundtrip |
test_config.py | — | Side-quest registry placeholder (none-only), model config fields, strategy config consistency |
test_data_pipeline.py | — | Dataset splits (non-overlapping, deterministic), code extraction (markdown fences, preamble, indentation), tokenizer EOS/pad, reward aggregation |
test_trajectory_logger.py | — | TrajectoryLoggerCallback: JSONL writes, heuristic enrichment, game trace, strategy metrics, sampling controls, wandb integration (mocked) |
test_game_trajectory.py | 9 | Game best-case trajectory verification: mixed group rewards/advantages, coefficient scaling, degenerate groups, asymmetric splits, shaping, multi-property, multi-prompt batches, edge cases |
test_game_analysis.py | — | Branch classification logic in analyze_game.py, game metrics logging in game trainers (branch fractions, phase tracking) |
test_length_penalty.py | — | Length penalty reward function: linear scaling, coefficient sensitivity, empty/whitespace handling, interaction with tokenizer |
test_alternation_phase.py | — | Frozen best-response alternation phase cycling, simultaneous mode, adapter switching |
test_fake_sandbox.py | 11 | FakeSandbox for config-to-reward testing: always-pass/fail, partial scores, string matching, integration with make_quest_grpo_reward |
test_strategy_ablation.py | 12 | All strategy coefficients at zero produce zero contribution (length, game, shaping, entropy, KL), individual and combined |
test_eval_train_consistency.py | 13 | Eval/train mode flags set correctly, game logic checks training flag, reward logging guards, rewards_per_func shape consistency |
test_sandbox_failures_propagate.py | 8 | Sandbox errors propagate (creation failure raises, stderr in error, timeout returns error); documents current silent-failure bug |
@pytest.mark.gpu)| File | E2E Plan | What it tests | Markers |
|---|---|---|---|
test_training_eval_consistency.py | 3 | KL-from-base is zero for identical models, positive after perturbation, penalty sign in loss; entropy bonus reduces loss; dtype consistency (bfloat16) | gpu |
test_gradient_direction.py | 2, 3 | Single-step gradient direction; multi-step (5-step) token-level probability tracking; reward function consistency | gpu |
test_adapter_isolation.py | 5, 6 | Only LoRA weights change during training; changes are nonzero, no NaN, reasonable magnitudes; training one adapter doesn't affect others; property_checker always frozen; charlie updates when train_adversary=True | gpu |
test_behavioral_regression.py | 1, 10 | Completions shorten after short-reward training; tmp usage decreases after no-tmp reward; constant reward produces no dramatic shift; length penalty shortens completions | gpu, slow |
test_lora_logprobs_change.py | — | PEFT has trainable params; logprobs shift after training; save/reload preserves logprobs; eval load path matches | gpu, slow |
test_trainer_smoke.py | — | Each custom trainer (game, maxent, KL-from-base) completes 2 training steps without crashing | gpu, slow |
test_property_checker.py | 8b | Batched vs sequential property checking equivalence, edge cases, model competency (True/False format compliance, positive/negative detection) | gpu |
temp/test_pr1_validation.py | — | Temporary (delete after PR1). Qwen3-8B / Qwen2.5-7B VRAM checks, forward pass, LoRA application | gpu, slow |
@pytest.mark.sandbox)| File | E2E Plan | What it tests |
|---|---|---|
test_sandbox_stress.py | 3, 8 | 100 concurrent exec calls succeed with no output mixing; non-zero exit codes propagate; timeouts don't silently succeed |
test_podman_storage_fix.py | — | Verifies /tmp-based podman-hpc storage override works on nodes where /local/user/ is not provisioned |
| File | Purpose |
|---|---|
test_sandbox_clean_storage.py | Manual PooledSandbox.clean_storage() debugging |
test_orphaned_processes.py | Orphaned-process detection after timeout (3 tests, all xfail — sandbox genuinely leaves orphans) |
The toy setting is designed for fast diversity experiments comparing training strategies.
Compares 7 training arms on Qwen2.5-0.5B-Instruct across 5 simple Python tasks, measuring whether GAP (game-based adversarial training) produces more diverse solutions than entropy/KL/length baselines.
Arms: vanilla GRPO, entropy bonus (0.01/0.05/0.1), length penalty, KL-from-base, GAP (game)
bash scripts/experiments/toy_0.6b/run_all.sh
Same arms on Qwen3-1.7B. Includes a relaunch variant using Qwen3-8B as the property checker judge.
# Full batch (baseline + 7 arms)
bash scripts/experiments/toy_1.7b/run_all.sh
# GAP with 8B property judge (simultaneous + 3:1 alternation)
bash scripts/experiments/toy_1.7b/relaunch_gap_8b_judge.sh
uv run python scripts/eval/run_toy_diversity_eval.py \
--base_model Qwen/Qwen3-1.7B \
--adapter_path <output_dir>/final_model \
--ref_freqs <baseline_dir>/ref_freqs.json \
--num_completions 100
Outputs (on /projects/):
experiments/toy_0.6b/
├── env.sh # Shared environment (TRANSFORMERS_CACHE, etc.)
├── ref_freqs.json # Baseline property frequencies
├── logs/ # Slurm .out/.err per job
└── outputs/
├── vanilla_s0/ # Each arm has:
│ ├── checkpoint-50/ ... checkpoint-500/
│ ├── best_model/
│ └── final_model/
│ ├── adapter_model.safetensors
│ └── adapter_config.json
├── entropy_0.01_s0/
├── entropy_0.05_s0/
├── entropy_0.1_s0/
├── length_s0/
├── kl_s0/
└── gap_s0/ # Also contains charlie/ and property_checker/ subdirs
Full-scale diversity benchmark on Qwen3-8B-Base with APPS code generation, comparing GAP (game) against the vanilla / entropy / KL / length baselines across seeds.
bash scripts/experiments/qwen3_8b_base/run_diversity_benchmark.sh
Experiment outputs follow a naming convention that enables auto-discovery:
experiments/qwen3_8b_base/
├── outputs/
│ ├── game_none_s0/ # {strat}_{side_quest}_s{seed}; side_quest is always `none`
│ │ ├── checkpoint-50/
│ │ ├── checkpoint-100/
│ │ ├── final_model/ # Present when training completes
│ │ └── trajectories/
│ │ ├── trajectories_20260215_1234.jsonl
│ │ └── trajectories_latest.jsonl -> trajectories_20260215_1234.jsonl
│ ├── game_none_s1/
│ ├── entropy_bonus_none_s0/
│ └── ...
├── manifest.json # Launch manifest (job IDs, arms, output dirs)
└── logs/ # Slurm .out/.err files
{strat}_{side_quest}_s{seed} — side_quest is now always none (e.g. game_none_s0). The discovery parser still recognises the old side-quest names so historical experiment dirs keep parsing.{arm_name}_s{seed} (e.g. gap_8b_eps_sim_s0)trajectories_latest.jsonl: Symlink to the most recent trajectory file (auto-created)manifest.json: Written by launch scripts, records Slurm job IDs per armCheck on all arms in an experiment:
python scripts/experiment_status.py /path/to/experiment
python scripts/experiment_status.py /path/to/experiment --side-quest none
python scripts/experiment_status.py /path/to/experiment --strat game
Shows DONE/RUN/MISS status, checkpoint progress, and trajectory line counts for each arm.
# Direct file paths
python scripts/analyze_trajectories.py outputs/.../trajectories/*.jsonl
python scripts/analyze_trajectories.py --game --filter 100-300 outputs/.../trajectories/*.jsonl
# Auto-discover from experiment directory
python scripts/analyze_trajectories.py --experiment-dir /path/to/experiment --strat game
python scripts/analyze_trajectories.py --experiment-dir /path/to/experiment --side-quest none --heuristic num_functions
# Direct file paths
python scripts/analyze_game.py outputs/.../trajectories/*.jsonl
# Auto-discover from experiment directory
python scripts/analyze_game.py --experiment-dir /path/to/experiment --strat game --side-quest none
# From slurm log files (when trajectory logging is disabled)
python scripts/parse_game_logs.py <logfile>.out -o traj.jsonl
python scripts/analyze_game.py traj.jsonl
Analysis modes: --summary (default), --branches, --properties, --traces N, --plot, --heuristics, --phase-split.
This repository is released under the Apache License 2.0. See
NOTICE for required attribution.
The trainer files under src/unexploitable_search/train/trainers/ are copies of
TRL's GRPOTrainer (Copyright The HuggingFace
Team, Apache 2.0), modified for this project. Each carries an Apache header noting the
modifications.
The Apache 2.0 license covers this repository's code only. Datasets and model weights are downloaded at runtime, are not redistributed here, and remain governed by their own licenses:
| Asset | Used for | License |
|---|---|---|
codeparrot/apps (Hendrycks et al.) | APPS code-generation domain | MIT |
tatsu-lab/alpaca | Alpaca instruction-following domain | CC BY-NC 4.0 (non-commercial); derived from OpenAI text-davinci-003 outputs and subject to OpenAI's terms |
Qwen/Qwen3-* weights | Base / instruct models | Apache 2.0 |
google/gemma-3-*-it weights | Instruct / property-checker judge models | Gemma Terms of Use |
Note in particular that the Alpaca data is non-commercial: the Alpaca domain is suitable for academic / non-commercial research only, regardless of this repository's code license. Users are responsible for complying with each asset's license.
If you use this code, please cite the paper:
@misc{garl_without_demonstrations,
title = {GARL without demonstrations},
author = {Jacob Pfau and Arathi Mani and Alejandro Aristizabal and Stefan Jones and Xanthe Spence and Jazon Szabo},
year = {2026},
note = {https://github.com/AI-Safety-Institute/unexploitable-search}
}
Python
91.9%
Shell
8.1%