forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_issue300_online_dpo_train.py
More file actions
156 lines (129 loc) · 6.25 KB
/
Copy pathtest_issue300_online_dpo_train.py
File metadata and controls
156 lines (129 loc) · 6.25 KB
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
"""Issue #300 — validate a full online-DPO training run logs a reward metric.
``task='online_dpo'`` (v0.71.31) adapts to the installed TRL:
* **trl <1** — pairwise ``judge=`` (a ``BasePairwiseJudge``),
* **trl 1.x** — pointwise ``reward_funcs=[...]`` (pairwise judges were removed;
``OnlineDPOTrainer`` moved to ``trl.experimental.online_dpo``).
The pre-existing suite only validated ``setup()`` (trainer *build*) on trl 1.x —
never a real ``train()`` step on the ``reward_funcs`` path. This smoke test runs
``OnlineDPOTrainerWrapper.train()`` on a tiny model and
asserts a ``rewards/*`` entry appears in ``trainer.state.log_history``, proving
the reward signal is actually applied.
Version-agnostic by design: the wrapper adapts the synthetic length-preferring
evaluator to whichever API the installed trl exposes, so ``pytest -m smoke``
exercises the ``reward_funcs`` path under trl 1.x and the ``judge`` path under
trl <1. Marked ``smoke`` (it runs a real training loop), so it is excluded
from the default fast suite and run explicitly via ``pytest -m smoke``.
Executed live on the new dependency floor, trl 0.29.0 + transformers 5.12.1,
on Apple Silicon CPU: the real training loop completed and logged
rewards/chosen, rewards/rejected, rewards/accuracies, and rewards/margins. The
trl 1.x reward_funcs execution requires a trl>=1.7 + torch>=2.6 env (e.g. CI's
torch-2.6 job) — the same test body exercises it there without modification.
"""
from __future__ import annotations
import pytest
from soup_cli.eval.judge import JudgeScore
from soup_cli.trainer.online_dpo import _trl_has_judges
_TRL_HAS_JUDGES = _trl_has_judges()
class _LengthJudge:
"""Synthetic length-preferring Soup evaluator (prefers the LONGER response).
Exposes BOTH shapes so the wrapper can adapt it to either trl API:
``compare_pair`` (pairwise, trl <1) and ``evaluate`` (pointwise, trl 1.x
reward-func path).
"""
def __init__(self):
self.compare_calls = 0
self.evaluate_calls = 0
def compare_pair(self, prompt, resp_a, resp_b):
self.compare_calls += 1
if len(resp_a) == len(resp_b):
return -1
return 0 if len(resp_a) > len(resp_b) else 1
def evaluate(self, prompt, response, category="default"):
self.evaluate_calls += 1
return JudgeScore(
prompt=prompt, response=response, weighted_score=float(len(response))
)
def _reward_keys(log_history):
return sorted(
{k for entry in log_history for k in entry if k.startswith("rewards/")}
)
@pytest.mark.smoke
class TestOnlineDpoTrainLogsReward:
def test_train_logs_reward_metric(self, tmp_path, monkeypatch):
"""A few real online-DPO steps must log a rewards/* metric.
On trl 1.x this drives the reward_funcs path (the issue's gap); on trl
<1 it drives the judge path. Either way the reward signal is applied
and TRL logs rewards/*.
"""
import soup_cli.trainer.online_dpo as od
from soup_cli.config.loader import load_config_from_string
from soup_cli.trainer.online_dpo import OnlineDPOTrainerWrapper
monkeypatch.chdir(tmp_path)
cfg = load_config_from_string(
"base: hf-internal-testing/tiny-random-gpt2\n"
"task: online_dpo\n"
"data:\n train: x.jsonl\n max_length: 64\n"
"training:\n"
" online_dpo_judge: \"ollama://m\"\n"
" epochs: 1\n"
" batch_size: 2\n"
" online_dpo_max_new_tokens: 6\n"
" logging_steps: 1\n"
" lr: 1e-4\n"
"output: ./out\n"
)
# Enough prompts to exercise generation, judging, and an optimizer step.
dataset = {
"train": [
{"messages": [{"role": "user", "content": "hi there friend"}]},
{"messages": [{"role": "user", "content": "hello"}]},
{"messages": [{"role": "user", "content": "what is up"}]},
{"messages": [{"role": "user", "content": "yo"}]},
]
}
judge = _LengthJudge()
od._ONLINE_DPO_JUDGE_OVERRIDE = judge
try:
wrapper = OnlineDPOTrainerWrapper(cfg, device="cpu")
wrapper.setup(dataset)
result = wrapper.train()
finally:
od._ONLINE_DPO_JUDGE_OVERRIDE = None
assert result["total_steps"] >= 1, result
keys = _reward_keys(wrapper.trainer.state.log_history)
assert keys, (
"no rewards/* metric logged — the online-DPO reward signal was not "
f"applied. log_history keys: {wrapper.trainer.state.log_history}"
)
# The core online-DPO reward metrics TRL emits once a reward is applied.
assert "rewards/chosen" in keys
assert "rewards/rejected" in keys
if _TRL_HAS_JUDGES:
assert judge.compare_calls > 0, "TRL never called the pairwise judge"
else:
assert judge.evaluate_calls > 0, "TRL never called the reward function"
def test_reward_funcs_path_on_trl_1x(self, tmp_path, monkeypatch):
"""On trl 1.x the build must use the reward_funcs= API (not judge=).
Skips on trl <1 (pairwise judges present). Complements the train
smoke: it pins that the version the reward_funcs path targets actually
routes through reward_funcs, so the smoke above is exercising that path.
"""
if _TRL_HAS_JUDGES:
pytest.skip("trl <1 (pairwise judge API) — reward_funcs path is trl 1.x")
import soup_cli.trainer.online_dpo as od
from soup_cli.config.loader import load_config_from_string
from soup_cli.trainer.online_dpo import OnlineDPOTrainerWrapper
cfg = load_config_from_string(
"base: hf-internal-testing/tiny-random-gpt2\ntask: online_dpo\n"
"data:\n train: x.jsonl\n"
"training:\n online_dpo_judge: \"ollama://m\"\n"
)
od._ONLINE_DPO_JUDGE_OVERRIDE = _LengthJudge()
try:
wrapper = OnlineDPOTrainerWrapper(cfg, device="cpu")
built = wrapper._build_judge_or_reward(cfg.training)
finally:
od._ONLINE_DPO_JUDGE_OVERRIDE = None
assert "reward_funcs" in built
assert callable(built["reward_funcs"][0])
assert "judge" not in built