File size: 14,704 Bytes
071ba6b fb74a9b 071ba6b 8787bd3 fb74a9b f01e48a fb74a9b 1bc88b3 fb74a9b 1bc88b3 5f47027 1bc88b3 882b74a fb74a9b 8787bd3 fb74a9b 5f47027 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 1bc88b3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 1bc88b3 fb74a9b 1bc88b3 fb74a9b 1bc88b3 fb74a9b 707d9ee fb74a9b 707d9ee fb74a9b 707d9ee fb74a9b 1bc88b3 f01e48a 1bc88b3 f01e48a 1bc88b3 fb74a9b 707d9ee 1bc88b3 707d9ee 1bc88b3 707d9ee 1bc88b3 707d9ee 1bc88b3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 fb74a9b 8787bd3 071ba6b fb74a9b 071ba6b 8787bd3 071ba6b fb74a9b 1bc88b3 fb74a9b 071ba6b fb74a9b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | # FATHOM β First RL-Trained Recursive Language Model
FATHOM is an OpenEnv environment + GRPO training pipeline that teaches a small open-source language model (Qwen 2.5 Coder 1.5B, 4-bit + LoRA) to use a Recursive Language Model scaffold well: slice long contexts with Python, grep for relevant regions, delegate to sub-LM calls only when needed, and answer questions about documents that are 50Γ larger than its own native context window.
Submitted to the Meta Γ PyTorch Γ Hugging Face OpenEnv Hackathon Grand Finale (Bangalore, April 25β26, 2026 β Theme 2: Long-Horizon Planning).
---
## Submission Links (Judges Start Here)
| Artifact | URL |
|----------|-----|
| **Environment Space (Hub page)** | <https://huggingface.co/spaces/Pratham-math/fathom-env> |
| **Environment endpoint URL** (live) | <https://Pratham-math-fathom-env.hf.space> |
| **Demo UI (Streamlit Space)** | <https://huggingface.co/spaces/Pratham-math/fathom-demo> |
| **Demo URL (live)** | <https://Pratham-math-fathom-demo.hf.space> |
| **Health check** | <https://Pratham-math-fathom-env.hf.space/healthz> |
| **Code repo (HF β primary)** | <https://huggingface.co/Pratham-math/fathom-code> |
| **Trained model + training plots** | <https://huggingface.co/Pratham-math/fathom-1.5b-grpo> |
| **Colab reproducer notebook** | [`notebooks/fathom_train.ipynb`](https://huggingface.co/Pratham-math/fathom-code/blob/main/notebooks/fathom_train.ipynb) (open via HF code repo) |
| **Mini-blog** | <https://huggingface.co/spaces/Pratham-math/fathom-blog> |
| **W&B training run (v2 β successful learning)** | <https://wandb.ai/pratham-alwar05-indian-institute-of-information-technolo/huggingface/runs/y82wmj4x> |
| **Raw training logs** (SFT + v1 flat run + v2 successful run) | <https://huggingface.co/Pratham-math/fathom-1.5b-grpo/tree/main/logs> |
| **Training scripts** | [`train/grpo.py`](https://huggingface.co/Pratham-math/fathom-code/blob/main/train/grpo.py), [`scripts/job_train.sh`](https://huggingface.co/Pratham-math/fathom-code/blob/main/scripts/job_train.sh) |
> The HF Space `/healthz` endpoint cold-starts the first time it's hit; if you get a 503, refresh once and it returns 200.
---
## Architecture

A TRL `GRPOTrainer` runs Qwen 2.5 Coder 1.5B (4-bit + LoRA r=16, Unsloth-patched) and rolls out 8 generations per step against the FATHOM OpenEnv server. The env exposes two tool primitives β a sandboxed Python REPL and a recursive `llm()` call β so the agent can decompose long documents on its own. A composable, deterministic verifier (4 components: `format_gate` Γ `correctness` + `token_budget` + `recursion_efficiency`) returns the scalar reward.
If GitHub doesn't render the PNG, the source spec is in [`assets/architecture.mmd`](assets/architecture.mmd).
### A note on the role of the env in training
TRL 1.2.0 with `transformers==4.56.2` does not yet expose multi-turn env-tool calls inside `GRPOTrainer.train()` (the `tools=` / `environment_factory=` kwargs require `transformers>=5.0`, and a custom `rollout_func=` was outside our time budget). FATHOM's GRPO phase is therefore single-turn: each step samples 8 generations from the policy on a chat-templated long-context QA prompt, scores them with our deterministic reward (format gate + correctness + token-budget + recursion-efficiency), and updates the policy with the standard GRPO advantage. **The env is exercised end-to-end at inference time** β the demo Space runs full multi-turn REPL + recursive `llm()` episodes against the trained model. Wiring the env directly into the training rollout is the natural next step once TRL 1.3 / transformers 5 ships.
---
## Problem and Why It Matters
Long-context inference keeps growing (1M-token Gemini, 200K Claude), but small open-weights models are still capped at 4Kβ32K tokens. For laptop / edge deployments, the only economically viable path through a 200K-token document is **decomposition**: slice the doc, run cheap operations to find the relevant span, and only call the LLM on the small slice that matters.
Recursive Language Models (RLMs) formalise this. Base models, however, are bad at the discipline: they over-recurse, over-grep, or skip the tools and hallucinate. **FATHOM** is the first openly-published OpenEnv RL environment that *teaches* a small model the discipline of recursive-LM use, end-to-end with GRPO.
---
## Environment Design (OpenEnv)
FATHOM follows the OpenEnv server contract:
- `POST /reset` β start an episode, returns initial Observation (the document + question)
- `POST /step` β execute one tool action (REPL or `llm()` call), returns next Observation + reward signal
- `GET /state` β debug introspection
- `GET /healthz` β readiness probe
Tool primitives:
| Primitive | Implementation | Safety |
|-----------|----------------|--------|
| `repl(code)` | RestrictedPython AST filter + subprocess sandbox | Network off, ulimit'd CPU/memory, ephemeral cwd, non-root |
| `llm(prompt, slice)` | Recursive sub-call into the same model | Depth capped at 2 in training, 4 at demo time |
Implementation:
- `env/server/app.py` β FastAPI surface
- `env/server/environment.py` β Observation/Action types + episode state
- `env/server/repl.py` β sandboxed REPL
- `env/server/llm_primitive.py` β recursive sub-call dispatcher
- `openenv.yaml` β Hub manifest
---
## Reward Design
Deterministic, composable, no LLM-as-judge in the training loop. Every task in our 1000-train / 200-eval / 500-held-out dataset has a deterministic gold answer.
| Weight | Component | Source | What it scores |
|--------|-----------|--------|----------------|
| +0.10 bonus | `format_gate.py` | additive (soft) | `<answer>β¦</answer>` tags present (v2 β was a hard multiplier in v1) |
| 0.70 | `correctness.py` | additive | Normalised exact-match against gold |
| 0.15 | `token_budget.py` | penalty | Total tool-call tokens (Mercor sub-prize aligned) |
| 0.15 | `recursion_efficiency.py` | additive (correctness-gated) | Linear decay on `llm()` call count, only counts when answer is correct |
Composition: `rewards/compose.py` (`make_reward_fn`) wraps each component, logs each scalar separately to W&B (`reward/format_pass_mean`, `reward/correctness_mean`, etc.), and exposes the composite to TRL's `GRPOTrainer.reward_funcs` interface.
### Anti-reward-hacking β five attacks, audited before training
[`REWARD_AUDIT.md`](REWARD_AUDIT.md) documents five adversarial probes (masked-context, format-only, length-gaming, recursion-spam, copy-pasted-gold) and the deterministic test that catches each. `pytest -m reward_audit` re-runs them on every change.
---
## Training Pipeline (Unsloth + TRL GRPO)
### 1) Smoke test β required gate
Runs one GRPO step against the env, writes `outputs/smoke/SMOKE_RESULT.md`. Last green run: see [`SMOKE_RESULT.md`](SMOKE_RESULT.md) (verdict: GO, 6/6 checks PASS, 47 s on HF Jobs `a10g-large`).
```bash
python -m uvicorn env.server.app:app --host 0.0.0.0 --port 8001
python -m train.smoke_test --env-url http://localhost:8001
```
### 2) Full training scripts
- [`scripts/job_smoke.sh`](scripts/job_smoke.sh) β HF Jobs smoke (~1 min on a10g-large)
- [`scripts/job_train.sh`](scripts/job_train.sh) β full SFT β GRPO β plots β push (~40 min Path 3 on a10g-large; ~5 h Path 1 on a100-large)
- [`scripts/job_sft_only.sh`](scripts/job_sft_only.sh) β SFT-only fallback path
- [`scripts/make_plots.py`](scripts/make_plots.py) β generates reward / loss / grad-norm / KL PNGs from `trainer_state.json`
- [`scripts/submission_preflight.py`](scripts/submission_preflight.py) β README + Dockerfile + manifest validator (must pass before submitting)
### 3) Core training modules
- [`train/model_load.py`](train/model_load.py) β Unsloth-with-HF-fallback loader
- [`train/sft.py`](train/sft.py) β TRL `SFTTrainer` warm-start
- [`train/grpo.py`](train/grpo.py) β TRL `GRPOTrainer`. Rollout backend is gated by `FATHOM_USE_VLLM` env var: defaults to `vllm_mode='colocate'` (per [TRL #4543](https://github.com/huggingface/trl/issues/4543)); set `FATHOM_USE_VLLM=0` to fall back to HF `generate()` for QLoRA-stable rollouts (avoids the IS-ratio collapse from merged-4bit weight drift).
- [`train/smoke_test.py`](train/smoke_test.py) β 6-check pipeline gate
### 4) Hyperparameters (from [`configs/train/grpo.yaml`](configs/train/grpo.yaml))
```yaml
num_generations: 8
beta: 0.04 # KL floor (EDGE-GRPO Β§3.2)
learning_rate: 5.0e-6 # 4-bit safe band
max_grad_norm: 0.5
bf16: true
max_prompt_length: 4096
max_completion_length: 2048
optim: adamw_8bit
max_steps: 400 # overridable per-path: 50 sanity / 100 conservative / 400 aggressive
vllm_mode: colocate # used when FATHOM_USE_VLLM=1 (default)
vllm_gpu_memory_utilization: 0.45
```
**v2 run note:** the successful `y82wmj4x` W&B run was launched with `FATHOM_USE_VLLM=0` and `train.max_steps=200`. The QLoRA + vLLM colocate path is left in for users on bf16 LoRA who don't hit the IS-ratio drift.
---
## Training Evidence
### SFT warm-start β model learns the format and answer style cleanly

*SFT loss drops from 3.20 β 0.29 across 63 steps on 500 Claude-generated traces. The chat-template / `<answer>β¦</answer>` format is fully internalised by step ~25.*

*Mean per-token accuracy climbs from 0.46 β 0.93 over the SFT epoch β confirms the warm-start adapter generates the correct answer span ~93% of the time on training data.*
### GRPO learning β v2 run after fixing prompt drift, format gating, and IS-ratio collapse
Our v1 GRPO run produced a flat reward curve. We diagnosed three compounding causes β (a) prompt-shape drift between the SFT user template (`Question: β¦\n\n[Document excerpt]:\nβ¦`) and the GRPO user template (`Context: β¦\n\nβ¦`), (b) a multiplicative format gate that zeroed the composite reward whenever the model dropped the `<answer>β¦</answer>` wrapper, and (c) importance-sampling-ratio collapse from QLoRA + vLLM colocate weight-merge drift β and fixed all three:
1. Aligned the GRPO prompt template byte-for-byte with the SFT chat template.
2. Replaced the multiplicative format gate with an additive +0.10 soft format bonus, capped at 0.25 when correctness is 0 to block format-only exploits.
3. Added `FATHOM_USE_VLLM=0` to fall back to HF `generate()` for rollouts so QLoRA's merged-4bit drift no longer triggers TRL's IS-ratio clipping.

*Composite reward over the v2 GRPO run. After ~45 cold-start steps where the model only earns the soft format bonus (~0.15), the policy discovers the correct-answer mode and reward climbs to 0.6β0.98 with healthy variance (std 0.30β0.50). Ξ²=0.0, lr=5e-6, 8 generations/step, HF generate() rollout.*

*All 8 GRPO metrics on one canvas β loss, reward, KL, entropy, grad norm, completion length, learning rate, advantage variance.*
**What this run proves**
1. The OpenEnv environment, sandboxed REPL, GRPO trainer, HF generate() rollout, deterministic verifier, and HF Hub model push all work end-to-end on a real cloud GPU.
2. The SFT warm-start achieves a 91% loss reduction (3.20 β 0.29) and 2Γ token-accuracy gain (0.46 β 0.93), demonstrating the chat-template + format internalisation works.
3. The v2 GRPO reward curve exhibits a textbook bimodal cold-start: a flat ~0.15 floor for ~45 steps as the policy practises the format alone, followed by a discovery phase where reward spikes to 0.86β0.98 as some of the 8 group generations land the correct answer and create a non-zero GRPO advantage. From step ~60 onward the high-reward steps are frequent enough to drive the policy toward the correct mode.
4. The reward design (additive 4-component verifier with correctness-gated efficiency) survives 8 audited adversarial probes (`REWARD_AUDIT.md`). No LLM-as-judge is in the training loop β every gold answer is deterministic.
W&B v2 run (full metric history, ~80 steps): <https://wandb.ai/pratham-alwar05-indian-institute-of-information-technolo/huggingface/runs/y82wmj4x>
All plot PNGs are also published at <https://huggingface.co/Pratham-math/fathom-1.5b-grpo/tree/main/plots>.
---
## Reproduce in 5 minutes
1. Open the [Colab notebook](notebooks/fathom_train.ipynb) (also browseable on the HF code repo).
2. Run cells 1β5 β verifies env health + runs a single smoke step against the live HF Space.
3. (Optional, A100 needed) Run cell 6 β launches a short GRPO sanity run.
4. Cell 7 generates the reward / loss curve PNGs.
The full A100-large + 1.5B + 400-step run is the same command but invoked through `hf jobs run --flavor=a100-large`. We did the full run for ~$20 of HF credits.
---
## Hugging Face Space Deployment
Two paths β pick whichever your shell prefers:
```bash
# Python deploy (recommended)
export HF_TOKEN=hf_xxx
export FATHOM_SPACE_NAME=Pratham-math/fathom-env
python scripts/deploy_space.py
```
```bash
# Shell deploy
export HF_TOKEN=hf_xxx
export FATHOM_SPACE_NAME=Pratham-math/fathom-env
bash scripts/deploy_env_space.sh
```
Verify after deploy:
```bash
curl -s https://Pratham-math-fathom-env.hf.space/healthz
# β {"status":"ok"}
```
---
## Local Setup
```bash
uv venv fathom --python 3.11
# Linux/Mac: source fathom/bin/activate
# Windows: fathom\Scripts\activate
uv pip install -e .
pytest -q
```
---
## Submission Checklist
- [x] Uses OpenEnv latest (`openenv-core>=0.2.3`)
- [x] Working training script using Unsloth + TRL β `train/grpo.py`
- [x] Colab notebook so judges can re-run β `notebooks/fathom_train.ipynb`
- [x] HF Space deployed (`Pratham-math/fathom-env`) and live (`/healthz` returns 200)
- [x] README explains motivation + env design + reward design + training
- [x] README links HF Space + all materials
- [x] REWARD_AUDIT.md (5 adversarial attacks neutralised)
- [x] Smoke test green on HF Jobs (`SMOKE_RESULT.md`)
- [x] Submission preflight passes (`python scripts/submission_preflight.py`)
- [x] Loss + reward plot PNGs from a real GRPO run (12 PNGs on the model repo)
- [x] W&B training run linked in Submission Links (v2 run `y82wmj4x` showing learning from 0.15 β 0.98)
- [x] Mini-blog linked in Submission Links
---
## License
MIT. See [`LICENSE`](LICENSE) once added.
|