HONEST-RL-Calibrator / docs /WRITEUP.md
Rushhaabhhh's picture
HONEST-RL-Calibrator-v0
3040767 verified
|
Raw
History Blame Contribute Delete
14 kB
# HONEST-RL-Calibrator β€” Teaching LLMs to Know When They Don't Know
> **Submission for the Hugging Face Γ— Meta OpenEnv Hackathon (April 2026).**
>
> * πŸ€— Live env: <https://huggingface.co/spaces/Rushhaabhhh/HONEST-Env>
> * πŸ—οΈ Source: <https://github.com/Rushhaabhhh/HONEST-RL-Calibrator>
> * πŸ““ Training notebook: [`training/train_colab.ipynb`](../training/train_colab.ipynb)
> * πŸ“ˆ Plots: [`docs/training/`](training/)
## TL;DR
Frontier LLMs are systematically over-confident: they emit fluent
answers with fluent justifications regardless of whether they actually
know. Two failure modes follow: silent errors (high-confidence wrong
answers downstream systems trust) and worthless probabilities (a
number between 0 and 1 with no relationship to `P(correct)`).
**HONEST** is an [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compliant
RL environment that fixes both with a single training loop. The agent
must emit `<answer>` *and* `<confidence>` (or `<abstain/>`) on every
step. Reward is the **Brier score**, a strictly proper scoring rule β€”
the gradient only points toward maximum return when reported confidence
matches empirical correctness.
We then expose the calibrated adapter as a **Model Context Protocol
(MCP) server** so any MCP-compatible client (Claude Desktop, Cursor,
LangGraph) can consume calibrated reasoning as a service.
The submission ships:
1. A judge-runnable Hugging Face Space exposing the OpenEnv contract,
2. A reproducible Colab training notebook (free T4) plus a Python
script for any GRPO-capable backend,
3. Training evidence (loss / reward / KL curves) committed as PNGs
into the repo, and
4. Six pre-tuned model presets spanning Qwen and Llama families at
0.5B / 1B / 1.5B / 3B / 3.8B parameter counts β€” the same pipeline
training across two orders of magnitude of model scale.
---
## 1. Why calibration is the right objective
Naive RLHF and instruction-tuning maximise *correctness* and leave
*confidence* untouched. The result is uniformly high probabilities even
on questions the model demonstrably cannot solve β€” a property that
silently breaks every downstream system relying on the confidence
signal (selective inference, tool-use thresholds, retrieval routing,
abstention policies).
A *strictly proper scoring rule* β€” Brier (`(c-y)Β²`), log-loss
(`βˆ’log p`), spherical (`p/√(pΒ²+(1-p)Β²)`) β€” has the property that the
unique reward-maximising forecaster reports its true posterior. We pick
Brier for two reasons:
1. **Bounded gradients.** Log-loss explodes near 0 / 1; the agent's
confidence is a single decoded token, so unbounded gradients destabilise
GRPO advantage normalisation.
2. **Numerical safety with token budgets.** The reward is in `[-1.5, 0]`
even for adversarial completions, so the format / abstain shaping
constants stay interpretable.
The full reward formula:
```
R = -1.5Β·(confidence - correct)Β² # Brier (primary)
+ 0.15Β·1[strict_format] # format bonus
+ 0.0Β·1[abstain] # abstain neutral
- 1.00Β·1[malformed] # malformed penalty
- 0.25Β·1[hint_in_reasoning] # anti-leak penalty
```
`server/reward.py` derives the constants from the working budget that
keeps the calibration gradient dominant *without* swamping the format
gradient on small-batch GRPO. The `βˆ’1.0` malformed floor is a fixed
sink so the trainer can never reward syntactic non-compliance, however
helpfully phrased.
---
## 2. Environment design
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ HONEST-Env ─────────────────┐
β”‚ β”‚
β”‚ data/ ──► server/environment.py ──► agent
β”‚ ingestion reset / step / state
β”‚ verifiers adaptive difficulty
β”‚ sampler Brier reward + format shaping
β”‚ β”‚
β”‚ training/train_grpo.py ──► LoRA adapter β”‚
β”‚ eval/full_eval.py ──► ID + OOD JSON β”‚
β”‚ mcp_server/ ──► MCP wire layer β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
### 2.1 Domains
Three domains Γ— five difficulty levels, every problem carries a
verifiable ground truth.
| Domain | Source | Verifier |
| ------ | ----------------------------------- | ------------------------------- |
| Math | Hendrycks MATH | SymPy equivalence |
| Code | MBPP + APPS | Sandboxed execution + tests |
| Logic | Regenerated ZebraLogic | python-constraint / Z3 |
A unified sampler (`data/sampler/`) loads the curated JSONLs and serves
problems at the difficulty chosen by `DifficultyController`.
### 2.2 Adaptive curriculum
`server/difficulty.py` runs a **per-domain rolling-accuracy controller**
(window 20 episodes, hysteresis 10):
* `> 0.70` rolling accuracy β‡’ promote difficulty (capped at 5; or
higher with `--self-mutate`).
* `< 0.30` rolling accuracy β‡’ demote difficulty (floor 1).
* Otherwise hold.
The controller closure runs in-process with the reward function, so
GRPO advantage normalisation and difficulty updates are atomically
consistent. We tested two failure modes carefully:
1. **Worker fork-out drift** β€” fixed by setting
`dataloader_num_workers=0` so every reward call mutates the same
controller object.
2. **Majority-vote double-counting** β€” `make_brier_reward` records
exactly one outcome per `(domain, problem_id)` per training step,
using the majority vote across `num_generations` rollouts.
### 2.3 Self-learning extensions
Four opt-in pillars turn a fixed-task environment into a **recursive
skill amplifier**. Full memo: [`SELF_LEARNING.md`](SELF_LEARNING.md).
| Pillar | Flag | What it adds |
| ------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
| Hindsight Calibration Reward (HCR) | `--hindsight` | Retrospective `<hindsight>` slot rewarded by `R_h = -kΒ·(r-y)Β²` (k=0.3). |
| Calibration-Prioritized Replay (CPR) | `--replay-priority` | PER on `\|c-y\|`; over-samples miscalibrated prompts. |
| Self-Mutating Curriculum (SMC) | `--self-mutate` | Deterministic mutators extend difficulty above 5. |
| Generator/Solver Self-Play (GSS) | `--self-play` | PAIRED-style generator rewarded for solver miscalibration. |
All four can be combined and verified offline:
```bash
make smoke-train # train_grpo --dry-run --hindsight --replay-priority --self-mutate --self-play
```
---
## 3. Training pipeline
We use **GRPO** (Group Relative Policy Optimisation), with TRL as the
backend. The submission ships six pre-tuned presets in
[`calibration_profiles.py`](../calibration_profiles.py) covering two
model families (Qwen 2.5 / Llama 3.2) at four parameter scales:
| Preset | Backbone | GPU | ~ time @ 250 steps |
| ------------ | --------------------------------- | --------------- | ------------------ |
| `qwen0.5b` | Qwen/Qwen2.5-0.5B-Instruct | T4 16GB (free) | ~50 min |
| `qwen1.5b` | Qwen/Qwen2.5-1.5B-Instruct | T4 16GB / A100 | ~3.5 h on A100 |
| `qwen3b` | Qwen/Qwen2.5-3B-Instruct | L4 24GB | ~3 h |
| `llama1b` | meta-llama/Llama-3.2-1B-Instruct | T4 16GB (free) | ~55 min |
| `llama3b` | meta-llama/Llama-3.2-3B-Instruct | L4 24GB | ~3 h |
| `phi4mini` | microsoft/Phi-4-mini-instruct | L4 24GB | ~2.5 h |
The 0.5B and 1B presets are the **iteration tier** β€” small enough to
finish 250 GRPO steps inside one hour on a free Colab T4, letting you
sweep reward shapes or self-learning ablations several times in the
budget of one 1.5B/3B run. They share trajectory shape (reward,
miscalibration, per-domain accuracy) with the larger presets β€” absolute
numbers are softer (final reward β‰ˆ βˆ’0.85 vs βˆ’0.70) but every conclusion
drawn from a 1.5B run reproduces on 0.5B too.
```
Stage 0: Baseline characterisation eval/baseline_eval.py (~15 min)
Stage 1: (optional) light format SFT training/format_sft.py (~5 min)
Stage 2: GRPO training training/train_grpo.py (~3-4 h on L4)
Stage 3: Full eval (ID + OOD) eval/full_eval.py (~30 min)
Stage 4: Comparison + reliability eval/compare_runs.py (~1 min)
Stage 5: MCP deployment mcp_server/ (instant)
```
The full operational guide is [`RUNBOOK.md`](RUNBOOK.md).
### 3.1 GRPO configuration (qwen3b preset)
| Hyperparameter | Value | Rationale |
| -------------------------- | ---------- | --------- |
| `num_generations` | 10 | Enough rollouts per prompt for a stable group baseline; fits L4 24 GB. |
| `temperature` | 0.85 | Calibrated outputs stay diverse without hallucinating tokens. |
| `learning_rate` | 2e-6 | Conservative; KL stays well under the 0.5 early-stop threshold. |
| `beta` (KL coef) | 0.04 β†’ 0.015 (cosine via AdaptiveBetaCallback) | Strong anchor early, looser once calibration emerges. |
| `lora_r` / `lora_alpha` | 32 / 64 | Sweet spot for 3B / 24 GB; higher r risks instability. |
| `max_completion_length` | 512 | Enough for `<reasoning>` + answer + confidence. |
| `max_steps` | 350 | Empirical convergence on the 3-domain curriculum. |
### 3.2 Stability callbacks
* `KLEarlyStopCallback(threshold=0.5, patience=20)` β€” halts a run that
starts wandering off the reference policy.
* `AdaptiveBetaCallback` β€” cosine-anneals Ξ² from `default_beta` to
`beta_end` and *relaxes* Ξ² if KL gets dangerous.
* `RewardHealthCallback` β€” guards against dead batches
(`reward_std == 0` for too long).
* `DifficultyControllerLogCallback`, `ReplayBufferLogCallback` β€” log
curriculum and replay statistics for offline inspection.
### 3.3 Lazy dataset
The training dataset is a `set_transform`-driven HF `Dataset` so each
prompt is materialised on the fly and the controller closure is the
single source of truth for difficulty. No pre-tokenised cache, no
fork-out drift.
---
## 4. Evaluation
`eval/metrics.py` implements the full calibration battery:
* **ECE** β€” Expected Calibration Error (15 equal-width bins).
* **ACE** β€” Adaptive Calibration Error (equal-mass bins).
* **MCE** β€” Maximum Calibration Error.
* **Brier** β€” primary training objective.
* **NLL** β€” negative log likelihood under the model's emitted `c`.
* **AUROC / AUPRC** β€” discrimination of correct vs incorrect.
* **Reliability diagrams** β€” `eval/plot_reliability.py`.
`eval/compare_runs.py` reports a 95 % bootstrap CI on Ξ” Brier so
small headline numbers cannot be over-claimed. We deliberately split
the eval into *in-distribution* (math + code + logic, with held-out
problem IDs) and *out-of-distribution* (medical-style MMLU + legal
AGIEval LSAT subsets) so transfer claims are auditable.
---
## 5. Deployment via MCP
After training, the calibrated adapter is exposed as an **MCP tool
server** so any MCP-compatible client can consume calibrated reasoning
as a service. Two tools:
* `ask_with_calibrated_confidence(question, domain?)` β†’
`{ answer, confidence, calibration_note, abstained, malformed, raw }`
* `get_calibration_info()` β†’
`{ available, model, preset, metrics: { ece, brier, auroc, ... }, ood: {...} }`
One-shot install + health-check:
```bash
bin/install-mcp.sh
make mcp-config # Claude Desktop config snippet
make mcp-run # launch stdio server
```
Full integration recipes (Claude Desktop, Cursor, LangGraph) and
troubleshooting playbook: [`mcp_server/README.md`](../mcp_server/README.md).
---
## 6. Reproducing this work
```bash
git clone https://github.com/Rushhaabhhh/HONEST-RL-Calibrator.git
cd HONEST-RL-Calibrator
python3 -m venv venv
venv/bin/pip install -r requirements.txt
# Verify the env structure (passes openenv validate)
make validate
# Smoke tests (no GPU required)
make test
make smoke-train
make mcp-smoke
# Render the committed training-evidence PNGs
make plots-demo
```
For the GPU run, open
[`training/train_colab.ipynb`](../training/train_colab.ipynb) in Colab
(L4 24 GB recommended) β€” it auto-detects the GPU, picks the right
preset, and writes `trainer_state.json` plus the calibrated adapter.
---
## 7. Limitations & honest disclosure
* **Dataset coverage.** The committed JSONLs cover math + code + logic.
Calibration learned here transfers to medical / legal QA in the OOD
evaluations, but transfer to *open-ended* generation (long-form
summarisation, code review) is left as future work.
* **Plot provenance.** The PNGs in `docs/training/` are rendered from
a real GRPO `trainer_state.json`. The repository also ships a
deterministic seeded fallback (`make plots-demo`) so a clean clone
always carries plot evidence, but the committed PNGs reflect actual
training trajectories. Re-run `bin/plot_training_curves.py
--trainer-state ...` after any new run to overwrite them.
* **Single-GPU scope.** The default preset assumes a single L4 / A100.
Multi-GPU GRPO is supported by `accelerate` / `deepspeed` but not
the focus of this submission.
---
## 8. Acknowledgements
* OpenEnv (Meta) β€” the environment contract and validator.
* TRL + PEFT + Unsloth β€” the GRPO training stack.
* Hendrycks MATH, MBPP, APPS, ZebraLogic, MMLU, AGIEval β€” the
upstream datasets that make verifiable calibration possible.