The RL capstone: post-train Llama-3.2-3B with GRPO using a representation-level reward derived from RTV — rewarding generations whose internal activation trajectory looks safe, rather than an external content judge:
attack (dir-GCG) → detect (RTV) → defend (GRPO with an RTV reward)
Status: end-to-end validated on Llama-3.2-3B. The RTV internal-trajectory reward
defends against held-out GCG attacks (100% → 6.9–10.3% ASR, within and cross-corpus)
as well as a guard content classifier (parity, within the measured noise floor), at no
helpfulness cost — and it also holds against a fully adaptive attacker that re-optimizes
dir-GCG against the defended geometry (clean guard-ASR 32% → 9% on MaliciousInstruct-100),
using only the model's own refusal-direction geometry. Full numbers in
RESULTS.md.
| eval set (n) | base | RTV reward | guard reward |
|---|---|---|---|
| attack-within — MI+HarmfulQA held-out (29) | 100.0% | 10.3% | 17.2% |
| attack-cross — JBB, never seen (29) | 100.0% | 6.9% | 6.9% |
| AdvBench plain-harmful, iid (104) | 1.0% | 0.0% | 0.0% |
| HarmBench plain-harmful, OOD (200) | 4.5% | 3.0% | 3.0% |
| XSTest over-refusal (120) | 2.5% | 3.3% | 3.3% |
For each prompt, sample a GROUP of G completions from the current policy; score each;
the advantage of completion i is its group-normalized reward
A_i = (r_i − mean_group) / (std_group + eps). The group mean is the baseline, so GRPO
needs no value/critic network (its main simplification over PPO). Update with a
clipped ratio objective + KL to the reference (LoRA-adapter-disabled base = one copy of
weights). TRL's GRPOTrainer handles rollouts, advantage, clipping, and KL.
attack/run_dirgcg.py optimizes a portable string suffix (token-invariant) so a
mid-layer hidden state aligns with the StrongREJECT refusal direction, with a behavioral
early-stop. attack/verify_guard.py then guard-confirms each suffix
(p_harm ≥ 0.5 and not is_refusal, judged on the clean goal), and attack/build_jbgcg.py
pools the survivors → 116 confirmed jailbreaks across 3 disjoint corpora (MI-32,
HarmfulQA-55, JBB-29). JBB is held out entirely for cross-corpus eval.
rtv/ reimplements Representation Trajectory Verification natively on a dedicated, exact-disjoint calibration pool. Per-layer refusal
direction (diff-in-means) → fingerprint F[l,p] = cos(r_l, h_l^p) over layers × last-5
positions → two-class Ledoit-Wolf Mahalanobis M(x) = min(d_harmless, d_harmful),
thresholded at 5% FPR. Depth-diverse operating set [14,16,25,26]: TPR 0.95 @ 5% FPR.
The reward must score the completion (a prompt-side score is group-constant in GRPO →
zero advantage → no gradient). reward/rtv_fingerprint.py fits a two-class LW-Mahalanobis
model on completion refusal-direction fingerprints (layers [10,14,22,26,28], CV-selected
by reward gap); reward/rtv_reward.py returns r_safe = tanh(LLR/2) = 2·P(safe|x) − 1
from a frozen reference model, in a frontier form (harmful → reward a safe
trajectory, benign → reward a non-refusal, so RL can't win by always refusing).
train/train_grpo.py runs TRL's GRPOTrainer with LoRA; --reward {scalar,rtv} swaps
the reward for the head-to-head.
Identical config/data/eval, only the reward swapped. On held-out attacks
(runs/eval_headtohead_final.json):
| eval set (n) | base | RTV reward | guard reward |
|---|---|---|---|
| attack-within (29) | 100% | 10.3% | 17.2% |
| attack-cross / JBB (29) | 100% | 6.9% | 6.9% |
| XSTest over-refusal (120) | 2.5% | 3.3% | 3.3% |
Parity, not superiority: RTV ≤ guard in every attack cell, but the within-set gap
(10.3% vs 17.2% = 2 responses) is inside the ±1-response reproducibility floor — a representation-only reward matches a strong external classifier. No
over-refusal collapse, plain-harmful refusal preserved. A train-only recalibration of the
reward detector — excluding every eval-attack completion, and even re-selecting layers to
[8,14,28] — reproduces this RTV column to the record (runs/eval_headtohead_airtight.json),
so the defense does not depend on having calibrated on eval data.
Adaptive robustness (the primary robustness result). Beyond transferring the base
attack, the defense holds against an attacker who re-optimizes dir-GCG from scratch against
the defended geometry — directions rebuilt on the defended model, target layer re-selected on
it (12→9), full base budget. On all 100 MaliciousInstruct goals, clean guard-confirmed ASR
falls 32% → 9% (~70% relative). Base 32/100 is exactly the MI confirmed-suffix count from
Stage 1 — the fresh attack yield on raw goals, not the pre-confirmed eval sets where base is
100% by construction. The reduction has two independent sources: the defended model refuses
more (72% vs 44%) and its non-refusals are far less often harmful (36% vs 57%). See
RESULTS.md §3b for the full breakdown, limits, and reproduction.
RESULTS.mdThe complete numbers, methodology, and honest caveats live in
RESULTS.md:
[8,14,28])
reproduces §3 to within one response → the result is invariant to detector
specification, not an artifact of calibrating on eval data.eval/reward_hacking.py buckets held-out completions by (actually-harmful? ×
looks-safe-inside?); if the RTV-trained policy has more mass in the HACKED cell
(harmful yet looks-safe-inside) than the scalar-trained one, the representation reward was
gamed. This is scaffolded as the natural next probe on top of the validated defense.
The LoRA adapters are not in git (large binaries) — download the three cited adapters from the
v1.0 GitHub Release and
unzip each into its runs/*/adapter/ path, or retrain from scratch with the commands below.
| adapter | release asset | unzip into |
|---|---|---|
| RTV reward (§3) | adapter_rtv.zip | runs/grpo_defend_flash_rtv/adapter/ |
| guard reward (baseline) | adapter_guard.zip | runs/grpo_defend_flash_guard/adapter/ |
| RTV airtight (§3a) | adapter_rtv_airtight.zip | runs/grpo_defend_flash_rtv_airtight/adapter/ |
curl -L -o adapter_rtv.zip \
https://github.com/kemalderya/grpo-rtv-reward/releases/download/v1.0/adapter_rtv.zip
unzip adapter_rtv.zip -d runs/grpo_defend_flash_rtv/adapter
# then: PeftModel.from_pretrained(base_model, "runs/grpo_defend_flash_rtv/adapter")
pip install -r requirements.txt
# offline HF env used throughout:
# HF_HOME=~/Desktop/ET/cache HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
# Stage 3 — defend: train the two adapters (RTV reward vs guard reward), same config
python -m src.train.train_grpo --config configs/grpo_defend_flash.yaml \
--data data/rollout_defend_flash.jsonl --reward rtv --out runs/grpo_defend_flash_rtv
python -m src.train.train_grpo --config configs/grpo_defend_flash.yaml \
--data data/rollout_defend_flash.jsonl --reward scalar --out runs/grpo_defend_flash_guard
# Evaluate base vs both adapters (matches the attack's decode: flash + bs=1 + native template)
python -m src.eval.generalize_check \
--adapters rtv:runs/grpo_defend_flash_rtv/adapter guard:runs/grpo_defend_flash_guard/adapter \
--batch-size 1 --out runs/eval_headtohead_final.json
The scripts/*.sh helpers (run_mvp.sh, build_fingerprint.sh, run_rtv.sh) are the
earlier plain-prompt MVP entry points and predate the attack-suffixed defend pipeline
above; prefer the module commands.
configs/ grpo_defend_flash.yaml (final head-to-head), grpo_defend*.yaml, grpo_smoke.yaml
src/attack/ run_dirgcg.py, verify_guard.py (guard-confirm), build_jbgcg.py (pool corpus),
build_airtight_directions.py, ablate_direction_asr.py (layer pick),
ablate_attack_layers.py, confirm_attack.py (§3b adaptive attack on defended model)
src/rtv/ datasets.py (disjoint pool), extract_states.py, detector.py, evaluate.py, ablate.py
src/reward/ scalar_reward.py (guard frontier), rtv_fingerprint.py, rtv_reward.py (novel),
build_completion_calib.py, completion_ablate.py
src/train/ train_grpo.py (TRL GRPO, --reward scalar|rtv)
src/eval/ evaluate.py (ASR/over-refusal), generalize_check.py (base-vs-adapters table),
reward_hacking.py (extension probe)
src/ model_util.py (single flash loader), template_util.py (render_native)
2 commits
Python
99.0%
The RL capstone: post-train Llama-3.2-3B with GRPO using a representation-level reward derived from RTV — rewarding generations whose internal activation trajectory looks safe, rather than an external content judge:
attack (dir-GCG) → detect (RTV) → defend (GRPO with an RTV reward)
Status: end-to-end validated on Llama-3.2-3B. The RTV internal-trajectory reward
defends against held-out GCG attacks (100% → 6.9–10.3% ASR, within and cross-corpus)
as well as a guard content classifier (parity, within the measured noise floor), at no
helpfulness cost — and it also holds against a fully adaptive attacker that re-optimizes
dir-GCG against the defended geometry (clean guard-ASR 32% → 9% on MaliciousInstruct-100),
using only the model's own refusal-direction geometry. Full numbers in
RESULTS.md.
| eval set (n) | base | RTV reward | guard reward |
|---|---|---|---|
| attack-within — MI+HarmfulQA held-out (29) | 100.0% | 10.3% | 17.2% |
| attack-cross — JBB, never seen (29) | 100.0% | 6.9% | 6.9% |
| AdvBench plain-harmful, iid (104) | 1.0% | 0.0% | 0.0% |
| HarmBench plain-harmful, OOD (200) | 4.5% | 3.0% | 3.0% |
| XSTest over-refusal (120) | 2.5% | 3.3% | 3.3% |
For each prompt, sample a GROUP of G completions from the current policy; score each;
the advantage of completion i is its group-normalized reward
A_i = (r_i − mean_group) / (std_group + eps). The group mean is the baseline, so GRPO
needs no value/critic network (its main simplification over PPO). Update with a
clipped ratio objective + KL to the reference (LoRA-adapter-disabled base = one copy of
weights). TRL's GRPOTrainer handles rollouts, advantage, clipping, and KL.
attack/run_dirgcg.py optimizes a portable string suffix (token-invariant) so a
mid-layer hidden state aligns with the StrongREJECT refusal direction, with a behavioral
early-stop. attack/verify_guard.py then guard-confirms each suffix
(p_harm ≥ 0.5 and not is_refusal, judged on the clean goal), and attack/build_jbgcg.py
pools the survivors → 116 confirmed jailbreaks across 3 disjoint corpora (MI-32,
HarmfulQA-55, JBB-29). JBB is held out entirely for cross-corpus eval.
rtv/ reimplements Representation Trajectory Verification natively on a dedicated, exact-disjoint calibration pool. Per-layer refusal
direction (diff-in-means) → fingerprint F[l,p] = cos(r_l, h_l^p) over layers × last-5
positions → two-class Ledoit-Wolf Mahalanobis M(x) = min(d_harmless, d_harmful),
thresholded at 5% FPR. Depth-diverse operating set [14,16,25,26]: TPR 0.95 @ 5% FPR.
The reward must score the completion (a prompt-side score is group-constant in GRPO →
zero advantage → no gradient). reward/rtv_fingerprint.py fits a two-class LW-Mahalanobis
model on completion refusal-direction fingerprints (layers [10,14,22,26,28], CV-selected
by reward gap); reward/rtv_reward.py returns r_safe = tanh(LLR/2) = 2·P(safe|x) − 1
from a frozen reference model, in a frontier form (harmful → reward a safe
trajectory, benign → reward a non-refusal, so RL can't win by always refusing).
train/train_grpo.py runs TRL's GRPOTrainer with LoRA; --reward {scalar,rtv} swaps
the reward for the head-to-head.
Identical config/data/eval, only the reward swapped. On held-out attacks
(runs/eval_headtohead_final.json):
| eval set (n) | base | RTV reward | guard reward |
|---|---|---|---|
| attack-within (29) | 100% | 10.3% | 17.2% |
| attack-cross / JBB (29) | 100% | 6.9% | 6.9% |
| XSTest over-refusal (120) | 2.5% | 3.3% | 3.3% |
Parity, not superiority: RTV ≤ guard in every attack cell, but the within-set gap
(10.3% vs 17.2% = 2 responses) is inside the ±1-response reproducibility floor — a representation-only reward matches a strong external classifier. No
over-refusal collapse, plain-harmful refusal preserved. A train-only recalibration of the
reward detector — excluding every eval-attack completion, and even re-selecting layers to
[8,14,28] — reproduces this RTV column to the record (runs/eval_headtohead_airtight.json),
so the defense does not depend on having calibrated on eval data.
Adaptive robustness (the primary robustness result). Beyond transferring the base
attack, the defense holds against an attacker who re-optimizes dir-GCG from scratch against
the defended geometry — directions rebuilt on the defended model, target layer re-selected on
it (12→9), full base budget. On all 100 MaliciousInstruct goals, clean guard-confirmed ASR
falls 32% → 9% (~70% relative). Base 32/100 is exactly the MI confirmed-suffix count from
Stage 1 — the fresh attack yield on raw goals, not the pre-confirmed eval sets where base is
100% by construction. The reduction has two independent sources: the defended model refuses
more (72% vs 44%) and its non-refusals are far less often harmful (36% vs 57%). See
RESULTS.md §3b for the full breakdown, limits, and reproduction.
RESULTS.mdThe complete numbers, methodology, and honest caveats live in
RESULTS.md:
[8,14,28])
reproduces §3 to within one response → the result is invariant to detector
specification, not an artifact of calibrating on eval data.eval/reward_hacking.py buckets held-out completions by (actually-harmful? ×
looks-safe-inside?); if the RTV-trained policy has more mass in the HACKED cell
(harmful yet looks-safe-inside) than the scalar-trained one, the representation reward was
gamed. This is scaffolded as the natural next probe on top of the validated defense.
The LoRA adapters are not in git (large binaries) — download the three cited adapters from the
v1.0 GitHub Release and
unzip each into its runs/*/adapter/ path, or retrain from scratch with the commands below.
| adapter | release asset | unzip into |
|---|---|---|
| RTV reward (§3) | adapter_rtv.zip | runs/grpo_defend_flash_rtv/adapter/ |
| guard reward (baseline) | adapter_guard.zip | runs/grpo_defend_flash_guard/adapter/ |
| RTV airtight (§3a) | adapter_rtv_airtight.zip | runs/grpo_defend_flash_rtv_airtight/adapter/ |
curl -L -o adapter_rtv.zip \
https://github.com/kemalderya/grpo-rtv-reward/releases/download/v1.0/adapter_rtv.zip
unzip adapter_rtv.zip -d runs/grpo_defend_flash_rtv/adapter
# then: PeftModel.from_pretrained(base_model, "runs/grpo_defend_flash_rtv/adapter")
pip install -r requirements.txt
# offline HF env used throughout:
# HF_HOME=~/Desktop/ET/cache HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
# Stage 3 — defend: train the two adapters (RTV reward vs guard reward), same config
python -m src.train.train_grpo --config configs/grpo_defend_flash.yaml \
--data data/rollout_defend_flash.jsonl --reward rtv --out runs/grpo_defend_flash_rtv
python -m src.train.train_grpo --config configs/grpo_defend_flash.yaml \
--data data/rollout_defend_flash.jsonl --reward scalar --out runs/grpo_defend_flash_guard
# Evaluate base vs both adapters (matches the attack's decode: flash + bs=1 + native template)
python -m src.eval.generalize_check \
--adapters rtv:runs/grpo_defend_flash_rtv/adapter guard:runs/grpo_defend_flash_guard/adapter \
--batch-size 1 --out runs/eval_headtohead_final.json
The scripts/*.sh helpers (run_mvp.sh, build_fingerprint.sh, run_rtv.sh) are the
earlier plain-prompt MVP entry points and predate the attack-suffixed defend pipeline
above; prefer the module commands.
configs/ grpo_defend_flash.yaml (final head-to-head), grpo_defend*.yaml, grpo_smoke.yaml
src/attack/ run_dirgcg.py, verify_guard.py (guard-confirm), build_jbgcg.py (pool corpus),
build_airtight_directions.py, ablate_direction_asr.py (layer pick),
ablate_attack_layers.py, confirm_attack.py (§3b adaptive attack on defended model)
src/rtv/ datasets.py (disjoint pool), extract_states.py, detector.py, evaluate.py, ablate.py
src/reward/ scalar_reward.py (guard frontier), rtv_fingerprint.py, rtv_reward.py (novel),
build_completion_calib.py, completion_ablate.py
src/train/ train_grpo.py (TRL GRPO, --reward scalar|rtv)
src/eval/ evaluate.py (ASR/over-refusal), generalize_check.py (base-vs-adapters table),
reward_hacking.py (extension probe)
src/ model_util.py (single flash loader), template_util.py (render_native)
2 commits
Python
99.0%