forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpreference.py
More file actions
242 lines (206 loc) · 9.87 KB
/
Copy pathpreference.py
File metadata and controls
242 lines (206 loc) · 9.87 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
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
"""Unified preference loss dispatcher (v0.40.0 Part B).
Provides a single entry-point for preference-style training that routes to
the per-loss wrappers (DPO / SimPO / ORPO / IPO / BCO). The legacy
``task: dpo``, ``task: simpo``, ... config forms remain first-class —
this module is purely additive.
Usage:
task: preference
training:
preference_loss: dpo # or simpo, orpo, ipo, bco
The dispatcher constructs a temporary :class:`SoupConfig` view with the
matching legacy task so the underlying TRL trainer is happy, then forwards
``setup`` / ``train`` to the right wrapper.
"""
from __future__ import annotations
from typing import Optional
from soup_cli.config.schema import SoupConfig
_SUPPORTED_LOSSES: frozenset[str] = frozenset({"dpo", "simpo", "orpo", "ipo", "bco"})
def is_multi_objective_preference(cfg: SoupConfig) -> bool:
"""True when the config asks for a weighted blend of preference losses.
v0.40.0 Part D — schema-level surface only; live runtime weighted
combination is deferred to v0.40.1 (mirrors the project's
stub-then-live pattern from v0.27.0 MII / v0.37.0 multipack /
v0.38.0 quant menu / v0.39.0 ReLoRA).
"""
weights = cfg.training.preference_loss_weights
return weights is not None and len(weights) >= 1
def get_loss_weights(cfg: SoupConfig) -> Optional[dict]:
"""Return a defensive copy of ``training.preference_loss_weights``."""
weights = cfg.training.preference_loss_weights
if weights is None:
return None
return dict(weights)
def resolve_preference_loss(cfg: SoupConfig) -> Optional[str]:
"""Return the preference loss name for a config, or ``None`` if N/A.
- ``task='preference'`` → ``training.preference_loss``.
- Legacy ``task in {dpo, simpo, orpo, ipo, bco}`` → that name (identity).
- Anything else → ``None``.
"""
if cfg.task == "preference":
return cfg.training.preference_loss
if cfg.task in _SUPPORTED_LOSSES:
return cfg.task
return None
def _make_inner_cfg(cfg: SoupConfig, loss: str) -> SoupConfig:
"""Build a SoupConfig view with task=<loss> so the inner wrapper can run.
Uses ``model_copy`` to round-trip without re-running validators on an
intermediate inconsistent state. The caller's cfg is never mutated.
"""
inner_training = cfg.training.model_copy(
update={"preference_loss": None, "preference_loss_weights": None}
)
return cfg.model_copy(update={"task": loss, "training": inner_training})
class PreferenceTrainerWrapper:
"""Dispatcher wrapper for ``task: preference`` configs.
Forwards to DPO / SimPO / ORPO / IPO / BCO wrappers based on
``training.preference_loss``.
"""
def __init__(
self,
config: SoupConfig,
device: str = "cuda",
report_to: str = "none",
deepspeed_config: Optional[str] = None,
fsdp_config: Optional[dict] = None,
trust_remote_code: bool = False,
):
self.config = config
self.device = device
self.report_to = report_to
self.deepspeed_config = deepspeed_config
self.fsdp_config = fsdp_config
self.trust_remote_code = trust_remote_code
self._inner = None
def _build_inner(self):
loss = self.config.training.preference_loss
if loss not in _SUPPORTED_LOSSES:
raise ValueError(
f"Unknown preference_loss={loss!r}. "
f"Supported: {sorted(_SUPPORTED_LOSSES)}"
)
inner_cfg = _make_inner_cfg(self.config, loss)
kwargs = {
"device": self.device,
"report_to": self.report_to,
"deepspeed_config": self.deepspeed_config,
"fsdp_config": self.fsdp_config,
"trust_remote_code": self.trust_remote_code,
}
if loss == "dpo":
from soup_cli.trainer.dpo import DPOTrainerWrapper
return DPOTrainerWrapper(inner_cfg, **kwargs)
if loss == "simpo":
from soup_cli.trainer.simpo import SimPOTrainerWrapper
return SimPOTrainerWrapper(inner_cfg, **kwargs)
if loss == "orpo":
from soup_cli.trainer.orpo import ORPOTrainerWrapper
return ORPOTrainerWrapper(inner_cfg, **kwargs)
if loss == "ipo":
from soup_cli.trainer.ipo import IPOTrainerWrapper
return IPOTrainerWrapper(inner_cfg, **kwargs)
# BCO — last branch by allowlist exhaustion.
from soup_cli.trainer.bco import BCOTrainerWrapper
return BCOTrainerWrapper(inner_cfg, **kwargs)
def _build_multi_objective(self):
"""v0.53.11 #68 — build the multi-objective primary trainer + combine hook.
Lifts the v0.40.1 "primary-loss approximation": the highest-weighted
loss still becomes the primary inner trainer (because each TRL
preference trainer owns its data collation), but a
:func:`combine_losses` wrapper now runs against the policy log-probs
in the inner trainer's ``compute_loss`` so each named loss
contributes to the backward pass per its weight.
BCO mixed with paired losses remains rejected at runtime (data-format
incompatible — paired needs ``prompt+chosen+rejected``, BCO needs
``prompt+completion+label``).
The math kernel ships in :mod:`soup_cli.utils.preference_combine`
and is exercised by ``test_v05311.py``. Live wrapper installation
on the inner Trainer's ``compute_loss`` is attached via
:func:`attach_weighted_preference_combine` after the inner setup
completes (called from :meth:`setup`).
"""
from rich.console import Console as _Console
from soup_cli.utils.preference_combine import (
describe_blend,
validate_weight_compat,
)
weights = get_loss_weights(self.config) or {}
validate_weight_compat(weights)
primary = max(weights, key=weights.get)
# Build the primary inner wrapper using the same dispatch table.
inner_cfg = _make_inner_cfg(self.config, primary)
kwargs = {
"device": self.device,
"report_to": self.report_to,
"deepspeed_config": self.deepspeed_config,
"fsdp_config": self.fsdp_config,
"trust_remote_code": self.trust_remote_code,
}
if primary == "dpo":
from soup_cli.trainer.dpo import DPOTrainerWrapper as PrimaryWrapper
elif primary == "simpo":
from soup_cli.trainer.simpo import SimPOTrainerWrapper as PrimaryWrapper
elif primary == "orpo":
from soup_cli.trainer.orpo import ORPOTrainerWrapper as PrimaryWrapper
elif primary == "ipo":
from soup_cli.trainer.ipo import IPOTrainerWrapper as PrimaryWrapper
elif primary == "bco":
from soup_cli.trainer.bco import BCOTrainerWrapper as PrimaryWrapper
else: # defensive — schema enforces the allowlist
raise ValueError(f"unknown primary preference loss: {primary!r}")
_Console().print(
f"[cyan]Multi-objective preference loss:[/] {describe_blend(weights)} "
f"(primary: {primary})"
)
self._active_weights = dict(weights)
return PrimaryWrapper(inner_cfg, **kwargs)
def setup(self, dataset: dict) -> None:
# v0.40.1 Part B — multi-objective live runtime (replaces v0.40.0
# Part D NotImplementedError stub). The combiner shares a single
# forward pass across the active losses; BCO mixed with paired
# losses is rejected at runtime (data-format incompatible).
if is_multi_objective_preference(self.config):
# v0.40.1 review fix — fail-fast compatibility check (BCO mixed
# with paired losses) BEFORE building the heavy primary trainer.
# Advisory print + build are owned by ``_build_multi_objective``
# so successive setup() calls (e.g. resume) emit one consistent
# advisory line, not two.
from soup_cli.utils.preference_combine import validate_weight_compat
weights = get_loss_weights(self.config) or {}
validate_weight_compat(weights)
if self._inner is None:
self._inner = self._build_multi_objective()
if self._inner is not None:
self._inner.setup(dataset)
# v0.53.11 #68 — attach the weighted-combine compute_loss
# wrapper after the inner trainer's setup completes so the
# TRL trainer instance is available.
from soup_cli.utils.preference_combine import (
attach_weighted_preference_combine,
)
inner_trainer = getattr(self._inner, "trainer", None)
if inner_trainer is not None:
try:
attach_weighted_preference_combine(inner_trainer, weights)
except (TypeError, ValueError) as exc:
# Schema validates weights at config load; runtime
# rejection should be loud — but fall through to the
# primary-loss-only path rather than crashing the run.
import logging
logging.getLogger(__name__).warning(
"attach_weighted_preference_combine skipped: %s", exc
)
return
if self._inner is None:
self._inner = self._build_inner()
self._inner.setup(dataset)
def train(self, **kwargs) -> dict:
if self._inner is None:
raise RuntimeError(
"PreferenceTrainerWrapper.train() called before setup(). "
"Call setup(dataset) first."
)
return self._inner.train(**kwargs)
@property
def trainer(self):
"""Expose the underlying HF Trainer for callbacks (HF push, eval-gate)."""
return getattr(self._inner, "trainer", None)