Project-Chronos 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chronos/__init__.py +16 -0
- chronos/_backend_legacy.py +64 -0
- chronos/app.py +122 -0
- chronos/backend/__init__.py +40 -0
- chronos/backend/dispatcher.py +347 -0
- chronos/backend/ext/__init__.py +2 -0
- chronos/backend/ext/opencl.py +22 -0
- chronos/cli.py +62 -0
- chronos/data/__init__.py +0 -0
- chronos/data/flexible_dataset.py +309 -0
- chronos/deps.py +72 -0
- chronos/eval/__init__.py +2 -0
- chronos/eval/benchmark.py +159 -0
- chronos/eval/io_profiler.py +232 -0
- chronos/io/__init__.py +13 -0
- chronos/io/async_prefetcher.py +220 -0
- chronos/io/cluster_layout.py +317 -0
- chronos/io/expert_store.py +559 -0
- chronos/io/io_simulator.py +40 -0
- chronos/io/storage.py +237 -0
- chronos/mlx/__init__.py +32 -0
- chronos/mlx/attention.py +230 -0
- chronos/mlx/expert_store.py +155 -0
- chronos/mlx/inference.py +159 -0
- chronos/mlx/model.py +181 -0
- chronos/mlx/moe.py +102 -0
- chronos/model/__init__.py +6 -0
- chronos/model/config.py +54 -0
- chronos/model/hf_io.py +149 -0
- chronos/model/hybrid_attention.py +237 -0
- chronos/model/lookahead_router.py +55 -0
- chronos/model/model_chronos.py +271 -0
- chronos/model/moe_chronos.py +189 -0
- chronos/model/temporal_loss.py +124 -0
- chronos/router/__init__.py +12 -0
- chronos/router/expert_predictor.py +163 -0
- chronos/router/intent_classifier.py +193 -0
- chronos/router/prefill_scheduler.py +248 -0
- chronos/runtime/__init__.py +2 -0
- chronos/runtime/cache_manager.py +117 -0
- chronos/runtime/inference_engine.py +295 -0
- chronos/runtime/metrics.py +75 -0
- chronos/serving/__init__.py +13 -0
- chronos/serving/vllm_adapter.py +85 -0
- chronos/trainer/__init__.py +6 -0
- chronos/trainer/chronos_trainer.py +151 -0
- chronos/trainer/distill_trainer.py +193 -0
- chronos/trainer/dpo_trainer.py +190 -0
- chronos/trainer/grpo_trainer.py +288 -0
- chronos/trainer/loss_mixin.py +165 -0
- chronos/trainer/optim_utils.py +101 -0
- chronos/trainer/orpo_trainer.py +189 -0
- chronos/trainer/reward.py +130 -0
- chronos/trainer/sft_trainer.py +189 -0
- chronos/tuning/__init__.py +1 -0
- chronos/tuning/_base_tuner.py +284 -0
- chronos/tuning/chronos_auto_tuner.py +203 -0
- project_chronos-0.1.1.dist-info/METADATA +670 -0
- project_chronos-0.1.1.dist-info/RECORD +83 -0
- project_chronos-0.1.1.dist-info/WHEEL +5 -0
- project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
- project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
- project_chronos-0.1.1.dist-info/top_level.txt +8 -0
- train_chronos.py +133 -0
- train_chronos_distill.py +95 -0
- train_chronos_dpo.py +82 -0
- train_chronos_grpo.py +88 -0
- train_chronos_orpo.py +81 -0
- train_chronos_sft.py +85 -0
- ui/__init__.py +3 -0
- ui/estimator.py +204 -0
- ui/gradio_compat.py +72 -0
- ui/i18n.py +540 -0
- ui/presets.py +194 -0
- ui/tabs/__init__.py +3 -0
- ui/tabs/autotune_tab.py +210 -0
- ui/tabs/benchmark_tab.py +196 -0
- ui/tabs/config_tab.py +318 -0
- ui/tabs/data_tab.py +148 -0
- ui/tabs/inference_tab.py +496 -0
- ui/tabs/iomon_tab.py +37 -0
- ui/tabs/pipeline_tab.py +134 -0
- ui/tabs/train_tab.py +1688 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/optim_utils.py
|
|
3
|
+
|
|
4
|
+
Shared optimizer + LR-schedule helpers used by every Chronos training stage
|
|
5
|
+
(pretrain, SFT, DPO, ORPO, GRPO, distill) and the WebUI Train tab.
|
|
6
|
+
|
|
7
|
+
Two reasons to centralize this:
|
|
8
|
+
|
|
9
|
+
1. Decay groups: AdamW with weight_decay applied uniformly to all params
|
|
10
|
+
destabilizes LayerNorm / RMSNorm scales and hurts embeddings. Standard
|
|
11
|
+
recipe is to exclude norms, biases, and embeddings from decay. Doing
|
|
12
|
+
this in one place means every trainer benefits from the same correct
|
|
13
|
+
recipe and we don't get drift.
|
|
14
|
+
|
|
15
|
+
2. Warmup-then-cosine schedule. minimind's get_lr is pure cosine (effective
|
|
16
|
+
LR at step 0 is already 0.55·base_lr), which destabilizes the first few
|
|
17
|
+
hundred steps with small batches. Linear warmup over the first ~5% of
|
|
18
|
+
steps then cosine decay to 10% of base is the boring-and-correct choice.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from typing import Iterable, Tuple
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
import torch.nn as nn
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _decay_groups(model: nn.Module) -> Tuple[list, list]:
|
|
30
|
+
"""Split trainable parameters into (decay, no_decay) lists.
|
|
31
|
+
|
|
32
|
+
No-decay: any parameter whose owning module is a LayerNorm/RMSNorm,
|
|
33
|
+
any 1-D parameter (typically bias / norm scale), and any embedding
|
|
34
|
+
weight (regardless of dim — embeddings are looked-up rows, not
|
|
35
|
+
rotated subspaces, so decay would push *all* tokens toward zero).
|
|
36
|
+
"""
|
|
37
|
+
decay, no_decay = [], []
|
|
38
|
+
seen = set()
|
|
39
|
+
for mod in model.modules():
|
|
40
|
+
is_norm = isinstance(mod, nn.LayerNorm) or mod.__class__.__name__.endswith("RMSNorm")
|
|
41
|
+
is_embed = isinstance(mod, nn.Embedding)
|
|
42
|
+
for p in mod.parameters(recurse=False):
|
|
43
|
+
if not p.requires_grad or id(p) in seen:
|
|
44
|
+
continue
|
|
45
|
+
seen.add(id(p))
|
|
46
|
+
if is_norm or is_embed or p.ndim < 2:
|
|
47
|
+
no_decay.append(p)
|
|
48
|
+
else:
|
|
49
|
+
decay.append(p)
|
|
50
|
+
return decay, no_decay
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_optimizer(
|
|
54
|
+
model: nn.Module,
|
|
55
|
+
lr: float,
|
|
56
|
+
*,
|
|
57
|
+
weight_decay: float = 0.01,
|
|
58
|
+
betas: Tuple[float, float] = (0.9, 0.95),
|
|
59
|
+
eps: float = 1e-8,
|
|
60
|
+
) -> torch.optim.AdamW:
|
|
61
|
+
"""AdamW with the standard transformer recipe applied to a Chronos model."""
|
|
62
|
+
decay, no_decay = _decay_groups(model)
|
|
63
|
+
return torch.optim.AdamW(
|
|
64
|
+
[
|
|
65
|
+
{"params": decay, "weight_decay": float(weight_decay)},
|
|
66
|
+
{"params": no_decay, "weight_decay": 0.0},
|
|
67
|
+
],
|
|
68
|
+
lr=float(lr), betas=betas, eps=eps,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_lr(
|
|
73
|
+
current_step: int,
|
|
74
|
+
total_steps: int,
|
|
75
|
+
base_lr: float,
|
|
76
|
+
*,
|
|
77
|
+
warmup_steps: int | None = None,
|
|
78
|
+
min_lr_ratio: float = 0.1,
|
|
79
|
+
) -> float:
|
|
80
|
+
"""Linear warmup → cosine decay to `min_lr_ratio · base_lr`.
|
|
81
|
+
|
|
82
|
+
`current_step` is 1-indexed (matches every trainer's `enumerate(loader, start=1)`).
|
|
83
|
+
Falls back to a constant `base_lr` when `total_steps <= 0`.
|
|
84
|
+
"""
|
|
85
|
+
if total_steps is None or total_steps <= 0:
|
|
86
|
+
return float(base_lr)
|
|
87
|
+
if warmup_steps is None:
|
|
88
|
+
warmup_steps = max(1, min(500, total_steps // 20))
|
|
89
|
+
step = max(1, int(current_step))
|
|
90
|
+
if step <= warmup_steps:
|
|
91
|
+
return float(base_lr) * (step / float(warmup_steps))
|
|
92
|
+
progress = (step - warmup_steps) / max(1, (total_steps - warmup_steps))
|
|
93
|
+
progress = min(1.0, max(0.0, progress))
|
|
94
|
+
cos = 0.5 * (1.0 + math.cos(math.pi * progress))
|
|
95
|
+
return float(base_lr) * (min_lr_ratio + (1.0 - min_lr_ratio) * cos)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def apply_lr(optimizer: torch.optim.Optimizer, lr: float) -> None:
|
|
99
|
+
"""Set the learning rate on every param-group in `optimizer`."""
|
|
100
|
+
for g in optimizer.param_groups:
|
|
101
|
+
g["lr"] = float(lr)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/orpo_trainer.py — Stage 4 ORPO trainer.
|
|
3
|
+
|
|
4
|
+
ORPO (Hong et al. 2024) combines supervised NLL on the chosen response
|
|
5
|
+
with an odds-ratio penalty on the rejected response, in a single objective
|
|
6
|
+
that does NOT require a separate reference model. This matters for
|
|
7
|
+
memory-constrained Chronos deployments: DPO needs two full models in
|
|
8
|
+
memory; ORPO needs one.
|
|
9
|
+
|
|
10
|
+
Loss:
|
|
11
|
+
L_NLL = −mean_token log p_θ(y_w | x) # supervised on chosen
|
|
12
|
+
L_OR = −log σ( β · log [ odds(y_w) / odds(y_l) ] )
|
|
13
|
+
where odds(y) = p_θ(y) / (1 - p_θ(y))
|
|
14
|
+
L_ORPO = L_NLL + λ · L_OR
|
|
15
|
+
|
|
16
|
+
Plus Chronos mixin (temporal / lookahead / aux) and router KL anchor.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from contextlib import nullcontext
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
import torch.nn.functional as F
|
|
26
|
+
from torch import optim
|
|
27
|
+
from torch.utils.data import DataLoader
|
|
28
|
+
|
|
29
|
+
import chronos.deps # noqa
|
|
30
|
+
from trainer.trainer_utils import Logger, is_main_process # type: ignore
|
|
31
|
+
from chronos.data.flexible_dataset import StreamingDPODataset as DPODataset
|
|
32
|
+
|
|
33
|
+
from chronos.model.config import ChronosConfig
|
|
34
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
35
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
36
|
+
from chronos.trainer.optim_utils import get_lr
|
|
37
|
+
from chronos.trainer.loss_mixin import (
|
|
38
|
+
chronos_loss_term,
|
|
39
|
+
collect_router_probs,
|
|
40
|
+
router_kl_anchor,
|
|
41
|
+
capture_reference_routing,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mean_logprob(logits: torch.Tensor, labels: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
46
|
+
"""Mean log-prob of `labels` under `logits`, averaged over masked positions."""
|
|
47
|
+
log_probs = F.log_softmax(logits, dim=-1)
|
|
48
|
+
lp = log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1) # [B, T]
|
|
49
|
+
lp = (lp * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0)
|
|
50
|
+
return lp
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ChronosORPOTrainer:
|
|
54
|
+
def __init__(self, model: ChronosForCausalLM, config: ChronosConfig, args, tokenizer):
|
|
55
|
+
self.model = model
|
|
56
|
+
self.config = config
|
|
57
|
+
self.args = args
|
|
58
|
+
self.device = args.device
|
|
59
|
+
self.beta = float(getattr(args, "beta", 0.1))
|
|
60
|
+
self.lambda_or = float(getattr(args, "lambda_or", 0.1))
|
|
61
|
+
|
|
62
|
+
device_type = "cuda" if "cuda" in str(self.device) else "cpu"
|
|
63
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
64
|
+
self.autocast_ctx = nullcontext() if device_type == "cpu" \
|
|
65
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
66
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
67
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
68
|
+
self.optimizer = build_optimizer(
|
|
69
|
+
model, lr=args.learning_rate,
|
|
70
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
71
|
+
)
|
|
72
|
+
self._router_ref = None
|
|
73
|
+
|
|
74
|
+
def set_calibration_batch(self, x: torch.Tensor):
|
|
75
|
+
self._router_ref = capture_reference_routing(self.model, x, self.device)
|
|
76
|
+
|
|
77
|
+
def train_step(self, batch, step, total_steps):
|
|
78
|
+
x_c = batch["x_chosen"].to(self.device)
|
|
79
|
+
x_r = batch["x_rejected"].to(self.device)
|
|
80
|
+
y_c = batch["y_chosen"].to(self.device)
|
|
81
|
+
y_r = batch["y_rejected"].to(self.device)
|
|
82
|
+
m_c = batch["mask_chosen"].to(self.device).float()
|
|
83
|
+
m_r = batch["mask_rejected"].to(self.device).float()
|
|
84
|
+
x = torch.cat([x_c, x_r], dim=0)
|
|
85
|
+
y = torch.cat([y_c, y_r], dim=0)
|
|
86
|
+
|
|
87
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
88
|
+
for pg in self.optimizer.param_groups:
|
|
89
|
+
pg["lr"] = lr
|
|
90
|
+
|
|
91
|
+
with self.autocast_ctx:
|
|
92
|
+
outputs, lookahead_probs = self.model(x)
|
|
93
|
+
B2 = x_c.shape[0]
|
|
94
|
+
logits_c = outputs.logits[:B2]
|
|
95
|
+
logits_r = outputs.logits[B2:]
|
|
96
|
+
|
|
97
|
+
lp_c = _mean_logprob(logits_c, y_c, m_c) # [B]
|
|
98
|
+
lp_r = _mean_logprob(logits_r, y_r, m_r)
|
|
99
|
+
|
|
100
|
+
# NLL on chosen
|
|
101
|
+
nll = -lp_c.mean()
|
|
102
|
+
|
|
103
|
+
# Odds-ratio term (numerically stable via log-space)
|
|
104
|
+
# log odds(y) = lp - log(1 - exp(lp)). Use log(1 - exp(lp)) ≈
|
|
105
|
+
# log1p(-exp(lp)) with clamping to avoid NaN at lp ≈ 0.
|
|
106
|
+
def _log_odds(lp):
|
|
107
|
+
one_minus = (1.0 - lp.exp()).clamp_min(1e-6)
|
|
108
|
+
return lp - one_minus.log()
|
|
109
|
+
log_odds_ratio = _log_odds(lp_c) - _log_odds(lp_r)
|
|
110
|
+
l_or = -F.logsigmoid(self.beta * log_odds_ratio).mean()
|
|
111
|
+
|
|
112
|
+
base = nll + self.lambda_or * l_or
|
|
113
|
+
|
|
114
|
+
loss = chronos_loss_term(
|
|
115
|
+
self.model, base, lookahead_probs, self.config,
|
|
116
|
+
aux_loss=outputs.aux_loss,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
anc_val = 0.0
|
|
120
|
+
lam = float(getattr(self.config, "lambda_router_anchor", 0.0))
|
|
121
|
+
if lam > 0.0 and self._router_ref is not None:
|
|
122
|
+
r4 = collect_router_probs(self.model)
|
|
123
|
+
if r4 is not None and r4.shape[1] > 1:
|
|
124
|
+
cur = r4.mean(dim=2)
|
|
125
|
+
ref = self._router_ref
|
|
126
|
+
T = min(cur.shape[1], ref.shape[1])
|
|
127
|
+
Bm = min(cur.shape[0], ref.shape[0])
|
|
128
|
+
anc = router_kl_anchor(cur[:Bm, :T, :], ref[:Bm, :T, :], lam)
|
|
129
|
+
loss = loss + anc
|
|
130
|
+
anc_val = float(anc.item())
|
|
131
|
+
|
|
132
|
+
la_val = 0.0
|
|
133
|
+
r4 = collect_router_probs(self.model)
|
|
134
|
+
if (r4 is not None and r4.shape[1] > 1
|
|
135
|
+
and lookahead_probs is not None
|
|
136
|
+
and self.config.lookahead_steps > 0):
|
|
137
|
+
la_val = float(lookahead_supervision_loss(
|
|
138
|
+
lookahead_probs, r4.mean(dim=2).detach(),
|
|
139
|
+
self.config.lookahead_steps,
|
|
140
|
+
).item())
|
|
141
|
+
|
|
142
|
+
loss = loss / self.args.accumulation_steps
|
|
143
|
+
|
|
144
|
+
self.scaler.scale(loss).backward()
|
|
145
|
+
|
|
146
|
+
if step % self.args.accumulation_steps == 0:
|
|
147
|
+
self.scaler.unscale_(self.optimizer)
|
|
148
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
|
|
149
|
+
self.scaler.step(self.optimizer)
|
|
150
|
+
self.scaler.update()
|
|
151
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
loss.item() * self.args.accumulation_steps,
|
|
155
|
+
float(nll.item()),
|
|
156
|
+
float(l_or.item()),
|
|
157
|
+
la_val,
|
|
158
|
+
anc_val,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def train_epoch(self, epoch, loader, iters, max_steps=None):
|
|
162
|
+
self.model.train()
|
|
163
|
+
total_steps = self.args.epochs * iters
|
|
164
|
+
for step, batch in enumerate(loader, start=1):
|
|
165
|
+
if max_steps is not None and step > max_steps:
|
|
166
|
+
break
|
|
167
|
+
loss, nll, lor, la, anc = self.train_step(
|
|
168
|
+
batch, epoch * iters + step, total_steps,
|
|
169
|
+
)
|
|
170
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
171
|
+
lr = self.optimizer.param_groups[-1]["lr"]
|
|
172
|
+
Logger(
|
|
173
|
+
f"[ORPO] Epoch[{epoch+1}/{self.args.epochs}]({step}/{iters}) "
|
|
174
|
+
f"loss:{loss:.4f} nll:{nll:.4f} or:{lor:.4f} "
|
|
175
|
+
f"la:{la:.4f} anchor:{anc:.4f} lr:{lr:.2e}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _save(self, epoch, step):
|
|
179
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
180
|
+
ckp = os.path.join(self.args.save_dir, f"orpo_{self.config.hidden_size}_moe.pth")
|
|
181
|
+
self.model.eval()
|
|
182
|
+
state = self.model.state_dict()
|
|
183
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
184
|
+
self.model.train()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_orpo_loader(data_path, tokenizer, max_seq_len, batch_size):
|
|
188
|
+
ds = DPODataset(data_path, tokenizer, max_length=max_seq_len)
|
|
189
|
+
return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/reward.py
|
|
3
|
+
|
|
4
|
+
Pluggable reward models for GRPO. Two built-ins:
|
|
5
|
+
|
|
6
|
+
- ``ToyReward`` — length + vocab-purity + repetition-penalty heuristic.
|
|
7
|
+
No external dependencies, used in smoke tests and when no real reward
|
|
8
|
+
model is configured.
|
|
9
|
+
- ``LMRewardModel`` — wraps an HF AutoModel that exposes a ``.get_score(...)``
|
|
10
|
+
method (the minimind convention, see
|
|
11
|
+
``minimind-master/trainer/trainer_utils.py:160``). Loads lazily so
|
|
12
|
+
importing this file never forces an HF network download.
|
|
13
|
+
|
|
14
|
+
Any callable ``score(prompt: str, response_tokens: List[int], tokenizer) -> float``
|
|
15
|
+
is accepted by ``ChronosGRPOTrainer``; the classes below simply package
|
|
16
|
+
that protocol.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TOY_VOCAB = {
|
|
24
|
+
"the", "quick", "brown", "fox", "machine", "learning", "model", "expert",
|
|
25
|
+
"router", "cluster", "memory", "cache", "prefetch", "latency", "throughput",
|
|
26
|
+
"tensor", "training", "inference", "decode", "weight", "gradient",
|
|
27
|
+
"softmax", "attention", "layer", "embedding", "vocab", "sequence",
|
|
28
|
+
"loss", "optimizer", "epoch", "batch", "step", "token", "logit",
|
|
29
|
+
"minimind", "chronos", "hybrid", "mixture", "of", "experts", "sparse",
|
|
30
|
+
"dense", "neural", "network", "transformer", "rope", "rotary", "kv",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ToyReward:
|
|
35
|
+
"""Zero-dependency heuristic reward: length + vocab-purity − repetition."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, target_len: int = 24, vocab: Optional[set] = None):
|
|
38
|
+
self.target_len = target_len
|
|
39
|
+
self.vocab = vocab or TOY_VOCAB
|
|
40
|
+
|
|
41
|
+
def __call__(self, prompt: str, response_tokens: List[int], tokenizer) -> float:
|
|
42
|
+
return self.score(prompt, response_tokens, tokenizer)
|
|
43
|
+
|
|
44
|
+
def score(self, prompt: str, response_tokens: List[int], tokenizer) -> float:
|
|
45
|
+
text = tokenizer.decode(response_tokens, skip_special_tokens=True)
|
|
46
|
+
words = text.split()
|
|
47
|
+
if not words:
|
|
48
|
+
return -1.0
|
|
49
|
+
length_reward = min(len(words) / max(self.target_len, 1), 1.0)
|
|
50
|
+
on_vocab = sum(1 for w in words if w.lower() in self.vocab)
|
|
51
|
+
vocab_purity = on_vocab / len(words)
|
|
52
|
+
dupes = sum(1 for a, b in zip(words[:-1], words[1:]) if a == b)
|
|
53
|
+
repetition_penalty = -dupes / max(len(words), 1)
|
|
54
|
+
return length_reward + vocab_purity + repetition_penalty
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LMRewardModel:
|
|
58
|
+
"""HF-backed reward model.
|
|
59
|
+
|
|
60
|
+
Wraps any ``AutoModel`` that exposes ``get_score(tokenizer, messages) -> float``
|
|
61
|
+
(the reward-model ABI used by minimind and several open reward-model
|
|
62
|
+
checkpoints). The model is loaded lazily on the first call so that
|
|
63
|
+
importing this module is free.
|
|
64
|
+
|
|
65
|
+
Example::
|
|
66
|
+
|
|
67
|
+
reward = LMRewardModel("path/to/internlm2-1_8b-reward")
|
|
68
|
+
r = reward.score(prompt, response_tokens, tokenizer)
|
|
69
|
+
|
|
70
|
+
The score is clipped to [-3.0, 3.0] to keep GRPO advantages well-scaled,
|
|
71
|
+
matching the minimind convention.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, model_path: str, device: str = "cpu",
|
|
75
|
+
dtype_str: str = "float16", score_clip: float = 3.0):
|
|
76
|
+
self.model_path = model_path
|
|
77
|
+
self.device = device
|
|
78
|
+
self.dtype_str = dtype_str
|
|
79
|
+
self.score_clip = float(score_clip)
|
|
80
|
+
self._model = None
|
|
81
|
+
self._tokenizer = None
|
|
82
|
+
|
|
83
|
+
def _load(self):
|
|
84
|
+
if self._model is not None:
|
|
85
|
+
return
|
|
86
|
+
import torch
|
|
87
|
+
from transformers import AutoTokenizer, AutoModel
|
|
88
|
+
dtype = torch.float16 if self.dtype_str == "float16" else torch.float32
|
|
89
|
+
self._tokenizer = AutoTokenizer.from_pretrained(
|
|
90
|
+
self.model_path, trust_remote_code=True,
|
|
91
|
+
)
|
|
92
|
+
self._model = AutoModel.from_pretrained(
|
|
93
|
+
self.model_path, torch_dtype=dtype, trust_remote_code=True,
|
|
94
|
+
).to(self.device).eval()
|
|
95
|
+
|
|
96
|
+
def __call__(self, prompt: str, response_tokens: List[int], tokenizer) -> float:
|
|
97
|
+
return self.score(prompt, response_tokens, tokenizer)
|
|
98
|
+
|
|
99
|
+
def score(self, prompt: str, response_tokens: List[int], tokenizer) -> float:
|
|
100
|
+
self._load()
|
|
101
|
+
response = tokenizer.decode(response_tokens, skip_special_tokens=True)
|
|
102
|
+
messages = [
|
|
103
|
+
{"role": "user", "content": prompt},
|
|
104
|
+
{"role": "assistant", "content": response},
|
|
105
|
+
]
|
|
106
|
+
s = self._model.get_score(self._tokenizer, messages)
|
|
107
|
+
return max(min(float(s), self.score_clip), -self.score_clip)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_reward_fn(spec: Optional[str]) -> callable:
|
|
111
|
+
"""Convert a CLI spec string into a reward callable.
|
|
112
|
+
|
|
113
|
+
Spec forms:
|
|
114
|
+
None | "" | "toy" → ToyReward()
|
|
115
|
+
"lm:/path/to/reward-model" → LMRewardModel(path)
|
|
116
|
+
"lm:/path/to/reward,device=cuda" → LMRewardModel(path, device="cuda")
|
|
117
|
+
"""
|
|
118
|
+
if not spec or spec == "toy":
|
|
119
|
+
return ToyReward()
|
|
120
|
+
if spec.startswith("lm:"):
|
|
121
|
+
body = spec[3:]
|
|
122
|
+
parts = body.split(",")
|
|
123
|
+
path = parts[0]
|
|
124
|
+
kwargs = {}
|
|
125
|
+
for kv in parts[1:]:
|
|
126
|
+
if "=" in kv:
|
|
127
|
+
k, v = kv.split("=", 1)
|
|
128
|
+
kwargs[k.strip()] = v.strip()
|
|
129
|
+
return LMRewardModel(path, **kwargs)
|
|
130
|
+
raise ValueError(f"unknown reward spec: {spec!r}")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/sft_trainer.py
|
|
3
|
+
|
|
4
|
+
Chronos-aware supervised fine-tuning trainer.
|
|
5
|
+
|
|
6
|
+
Built around minimind's SFTDataset (jsonl with `conversations` field,
|
|
7
|
+
tokenizer.apply_chat_template + per-assistant-token loss mask) so the data
|
|
8
|
+
pipeline is identical to the one in minimind-master/trainer/train_full_sft.py.
|
|
9
|
+
What we add:
|
|
10
|
+
|
|
11
|
+
1. The model returns ``(outputs, lookahead_probs)``, which minimind's trainer
|
|
12
|
+
does not know about. We forward it explicitly.
|
|
13
|
+
2. ``chronos_loss_term`` mixes in λ_balance·aux + λ_temporal·temporal +
|
|
14
|
+
λ_lookahead·lookahead on top of the base CE.
|
|
15
|
+
3. Optional ``router_kl_anchor`` pulls the current router distribution back
|
|
16
|
+
toward the pretrain reference (default λ=0.01 for SFT — mild drift is
|
|
17
|
+
OK). The reference is captured once at stage start.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
from contextlib import nullcontext
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
from torch import optim
|
|
27
|
+
from torch.utils.data import DataLoader
|
|
28
|
+
|
|
29
|
+
import chronos.deps # ensure minimind on sys.path
|
|
30
|
+
|
|
31
|
+
from trainer.trainer_utils import Logger, is_main_process # type: ignore
|
|
32
|
+
from chronos.data.flexible_dataset import StreamingSFTDataset
|
|
33
|
+
|
|
34
|
+
from chronos.model.config import ChronosConfig
|
|
35
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
36
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
37
|
+
from chronos.trainer.optim_utils import get_lr
|
|
38
|
+
from chronos.trainer.loss_mixin import (
|
|
39
|
+
chronos_loss_term,
|
|
40
|
+
collect_router_probs,
|
|
41
|
+
router_kl_anchor,
|
|
42
|
+
capture_reference_routing,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChronosSFTTrainer:
|
|
47
|
+
"""Stage 2 (SFT) trainer for Chronos."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, model: ChronosForCausalLM, config: ChronosConfig, args, tokenizer):
|
|
50
|
+
self.model = model
|
|
51
|
+
self.config = config
|
|
52
|
+
self.args = args
|
|
53
|
+
self.tokenizer = tokenizer
|
|
54
|
+
self.device = args.device
|
|
55
|
+
|
|
56
|
+
device_type = "cuda" if "cuda" in str(self.device) else "cpu"
|
|
57
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
58
|
+
self.autocast_ctx = (
|
|
59
|
+
nullcontext() if device_type == "cpu"
|
|
60
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
61
|
+
)
|
|
62
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
63
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
64
|
+
self.optimizer = build_optimizer(
|
|
65
|
+
model, lr=args.learning_rate,
|
|
66
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Router anchor reference — captured at the start of a stage
|
|
70
|
+
self._router_ref = None
|
|
71
|
+
self._calib_batch = None
|
|
72
|
+
|
|
73
|
+
def set_calibration_batch(self, input_ids: torch.Tensor):
|
|
74
|
+
"""Provide a representative input_ids batch; at stage start the
|
|
75
|
+
current (pre-SFT) model's router is frozen as the anchor teacher."""
|
|
76
|
+
self._calib_batch = input_ids
|
|
77
|
+
self._router_ref = capture_reference_routing(self.model, input_ids, self.device)
|
|
78
|
+
|
|
79
|
+
def train_step(self, input_ids, labels, step, total_steps):
|
|
80
|
+
input_ids = input_ids.to(self.device)
|
|
81
|
+
labels = labels.to(self.device)
|
|
82
|
+
|
|
83
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
84
|
+
for pg in self.optimizer.param_groups:
|
|
85
|
+
pg['lr'] = lr
|
|
86
|
+
|
|
87
|
+
with self.autocast_ctx:
|
|
88
|
+
outputs, lookahead_probs = self.model(input_ids, labels=labels)
|
|
89
|
+
ce_loss = outputs.loss
|
|
90
|
+
aux_loss = outputs.aux_loss
|
|
91
|
+
|
|
92
|
+
loss = chronos_loss_term(
|
|
93
|
+
self.model, ce_loss, lookahead_probs, self.config,
|
|
94
|
+
aux_loss=aux_loss,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Router KL anchor
|
|
98
|
+
anchor_val = 0.0
|
|
99
|
+
lam = float(getattr(self.config, "lambda_router_anchor", 0.0))
|
|
100
|
+
if lam > 0.0 and self._router_ref is not None:
|
|
101
|
+
router_4d = collect_router_probs(self.model)
|
|
102
|
+
if router_4d is not None and router_4d.shape[1] > 1:
|
|
103
|
+
cur = router_4d.mean(dim=2)
|
|
104
|
+
# Shape-align: calibration batch shape may differ; crop
|
|
105
|
+
# the shorter axis on both sides.
|
|
106
|
+
ref = self._router_ref
|
|
107
|
+
T = min(cur.shape[1], ref.shape[1])
|
|
108
|
+
B = min(cur.shape[0], ref.shape[0])
|
|
109
|
+
anchor = router_kl_anchor(
|
|
110
|
+
cur[:B, :T, :], ref[:B, :T, :], lam,
|
|
111
|
+
)
|
|
112
|
+
loss = loss + anchor
|
|
113
|
+
anchor_val = float(anchor.item())
|
|
114
|
+
|
|
115
|
+
# Standalone lookahead loss for logging
|
|
116
|
+
la_val = 0.0
|
|
117
|
+
router_4d = collect_router_probs(self.model)
|
|
118
|
+
if (router_4d is not None and router_4d.shape[1] > 1
|
|
119
|
+
and lookahead_probs is not None
|
|
120
|
+
and self.config.lookahead_steps > 0):
|
|
121
|
+
teacher = router_4d.mean(dim=2).detach()
|
|
122
|
+
la = lookahead_supervision_loss(
|
|
123
|
+
lookahead_probs, teacher, self.config.lookahead_steps,
|
|
124
|
+
)
|
|
125
|
+
la_val = float(la.item())
|
|
126
|
+
|
|
127
|
+
loss = loss / self.args.accumulation_steps
|
|
128
|
+
|
|
129
|
+
self.scaler.scale(loss).backward()
|
|
130
|
+
|
|
131
|
+
if step % self.args.accumulation_steps == 0:
|
|
132
|
+
self.scaler.unscale_(self.optimizer)
|
|
133
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
|
|
134
|
+
self.scaler.step(self.optimizer)
|
|
135
|
+
self.scaler.update()
|
|
136
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
loss.item() * self.args.accumulation_steps,
|
|
140
|
+
ce_loss.item(),
|
|
141
|
+
float(aux_loss.item()) if aux_loss is not None else 0.0,
|
|
142
|
+
la_val,
|
|
143
|
+
anchor_val,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def train_epoch(self, epoch, loader, iters, start_step=0, wandb=None, max_steps=None):
|
|
147
|
+
self.model.train()
|
|
148
|
+
start_time = time.time()
|
|
149
|
+
total_steps = self.args.epochs * iters
|
|
150
|
+
|
|
151
|
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
|
152
|
+
if max_steps is not None and step > max_steps:
|
|
153
|
+
break
|
|
154
|
+
loss, ce, aux, la, anc = self.train_step(
|
|
155
|
+
input_ids, labels, epoch * iters + step, total_steps,
|
|
156
|
+
)
|
|
157
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
158
|
+
elapsed = time.time() - start_time
|
|
159
|
+
eta = elapsed / max(step - start_step, 1) * (iters - step) // 60
|
|
160
|
+
lr = self.optimizer.param_groups[-1]['lr']
|
|
161
|
+
Logger(
|
|
162
|
+
f'[SFT] Epoch[{epoch+1}/{self.args.epochs}]({step}/{iters}) '
|
|
163
|
+
f'loss:{loss:.4f} ce:{ce:.4f} aux:{aux:.4f} '
|
|
164
|
+
f'la:{la:.4f} anchor:{anc:.4f} lr:{lr:.2e} eta:{eta:.1f}min'
|
|
165
|
+
)
|
|
166
|
+
if wandb:
|
|
167
|
+
wandb.log({
|
|
168
|
+
"sft_loss": loss, "sft_ce": ce, "sft_aux": aux,
|
|
169
|
+
"sft_lookahead": la, "sft_anchor": anc, "lr": lr,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (step % self.args.save_interval == 0 or step == iters) and is_main_process():
|
|
173
|
+
self._save(epoch, step)
|
|
174
|
+
|
|
175
|
+
def _save(self, epoch, step):
|
|
176
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
177
|
+
ckp = os.path.join(
|
|
178
|
+
self.args.save_dir,
|
|
179
|
+
f'sft_{self.config.hidden_size}_moe.pth',
|
|
180
|
+
)
|
|
181
|
+
self.model.eval()
|
|
182
|
+
state = self.model.state_dict()
|
|
183
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
184
|
+
self.model.train()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_sft_loader(data_path, tokenizer, max_seq_len, batch_size):
|
|
188
|
+
ds = StreamingSFTDataset(data_path, tokenizer, max_length=max_seq_len)
|
|
189
|
+
return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .chronos_auto_tuner import ChronosAutoTuner, ChronosSearchSpaceConfig
|