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,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/dpo_trainer.py — Stage 3 DPO trainer.
|
|
3
|
+
|
|
4
|
+
Implements DPO with a frozen reference Chronos model + Chronos loss mixin
|
|
5
|
+
(lookahead/temporal/balance preserved) + strong router KL anchor (default
|
|
6
|
+
λ=0.1) so the alignment gradients don't blow up the cluster cache layout.
|
|
7
|
+
|
|
8
|
+
Mirrors minimind's train_dpo.py loss but uses the Chronos forward signature
|
|
9
|
+
``(outputs, lookahead_probs) = model(x)`` and tolerates its extra return.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from contextlib import nullcontext
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
import torch.nn.functional as F
|
|
20
|
+
from torch import optim
|
|
21
|
+
from torch.utils.data import DataLoader
|
|
22
|
+
|
|
23
|
+
import chronos.deps # noqa
|
|
24
|
+
from trainer.trainer_utils import Logger, is_main_process # type: ignore
|
|
25
|
+
from chronos.data.flexible_dataset import StreamingDPODataset as DPODataset
|
|
26
|
+
|
|
27
|
+
from chronos.model.config import ChronosConfig
|
|
28
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
29
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
30
|
+
from chronos.trainer.optim_utils import get_lr
|
|
31
|
+
from chronos.trainer.loss_mixin import (
|
|
32
|
+
chronos_loss_term,
|
|
33
|
+
collect_router_probs,
|
|
34
|
+
router_kl_anchor,
|
|
35
|
+
capture_reference_routing,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _logits_to_log_probs(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
|
|
40
|
+
log_probs = F.log_softmax(logits, dim=-1)
|
|
41
|
+
return log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def dpo_loss(ref_lp: torch.Tensor, pol_lp: torch.Tensor, mask: torch.Tensor, beta: float) -> torch.Tensor:
|
|
45
|
+
ref_lp = (ref_lp * mask).sum(dim=1)
|
|
46
|
+
pol_lp = (pol_lp * mask).sum(dim=1)
|
|
47
|
+
B = ref_lp.shape[0]
|
|
48
|
+
ch_ref, rj_ref = ref_lp[:B // 2], ref_lp[B // 2:]
|
|
49
|
+
ch_pol, rj_pol = pol_lp[:B // 2], pol_lp[B // 2:]
|
|
50
|
+
pi_logr = ch_pol - rj_pol
|
|
51
|
+
rf_logr = ch_ref - rj_ref
|
|
52
|
+
return -F.logsigmoid(beta * (pi_logr - rf_logr)).mean()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ChronosDPOTrainer:
|
|
56
|
+
def __init__(self, model: ChronosForCausalLM, config: ChronosConfig, args, tokenizer):
|
|
57
|
+
self.model = model
|
|
58
|
+
self.config = config
|
|
59
|
+
self.args = args
|
|
60
|
+
self.tokenizer = tokenizer
|
|
61
|
+
self.device = args.device
|
|
62
|
+
self.beta = float(getattr(args, "beta", 0.1))
|
|
63
|
+
|
|
64
|
+
device_type = "cuda" if "cuda" in str(self.device) else "cpu"
|
|
65
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
66
|
+
self.autocast_ctx = nullcontext() if device_type == "cpu" \
|
|
67
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
68
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
69
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
70
|
+
self.optimizer = build_optimizer(
|
|
71
|
+
model, lr=args.learning_rate,
|
|
72
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Frozen reference model — DPO needs π_ref(y|x). Build a fresh
|
|
76
|
+
# instance and load the current state_dict (deepcopy fails on
|
|
77
|
+
# autograd-tracked buffers in some torch versions).
|
|
78
|
+
self.ref_model = model.__class__(config).to(self.device)
|
|
79
|
+
self.ref_model.load_state_dict(model.state_dict(), strict=False)
|
|
80
|
+
self.ref_model.eval().requires_grad_(False)
|
|
81
|
+
|
|
82
|
+
self._router_ref = None
|
|
83
|
+
|
|
84
|
+
def set_calibration_batch(self, x_calib: torch.Tensor):
|
|
85
|
+
self._router_ref = capture_reference_routing(self.model, x_calib, self.device)
|
|
86
|
+
|
|
87
|
+
def train_step(self, batch, step, total_steps):
|
|
88
|
+
x_c = batch["x_chosen"].to(self.device)
|
|
89
|
+
x_r = batch["x_rejected"].to(self.device)
|
|
90
|
+
y_c = batch["y_chosen"].to(self.device)
|
|
91
|
+
y_r = batch["y_rejected"].to(self.device)
|
|
92
|
+
m_c = batch["mask_chosen"].to(self.device).float()
|
|
93
|
+
m_r = batch["mask_rejected"].to(self.device).float()
|
|
94
|
+
x = torch.cat([x_c, x_r], dim=0)
|
|
95
|
+
y = torch.cat([y_c, y_r], dim=0)
|
|
96
|
+
m = torch.cat([m_c, m_r], dim=0)
|
|
97
|
+
|
|
98
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
99
|
+
for pg in self.optimizer.param_groups:
|
|
100
|
+
pg["lr"] = lr
|
|
101
|
+
|
|
102
|
+
with self.autocast_ctx:
|
|
103
|
+
with torch.no_grad():
|
|
104
|
+
ref_out = self.ref_model(x)
|
|
105
|
+
ref_logits = ref_out[0].logits if isinstance(ref_out, tuple) else ref_out.logits
|
|
106
|
+
ref_lp = _logits_to_log_probs(ref_logits, y)
|
|
107
|
+
|
|
108
|
+
outputs, lookahead_probs = self.model(x)
|
|
109
|
+
pol_lp = _logits_to_log_probs(outputs.logits, y)
|
|
110
|
+
base = dpo_loss(ref_lp, pol_lp, m, beta=self.beta)
|
|
111
|
+
|
|
112
|
+
# Mix in Chronos regularizers
|
|
113
|
+
loss = chronos_loss_term(
|
|
114
|
+
self.model, base, lookahead_probs, self.config,
|
|
115
|
+
aux_loss=outputs.aux_loss,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Router KL anchor (DPO default: strong)
|
|
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
|
+
router_4d = collect_router_probs(self.model)
|
|
123
|
+
if router_4d is not None and router_4d.shape[1] > 1:
|
|
124
|
+
cur = router_4d.mean(dim=2)
|
|
125
|
+
ref = self._router_ref
|
|
126
|
+
T = min(cur.shape[1], ref.shape[1])
|
|
127
|
+
B = min(cur.shape[0], ref.shape[0])
|
|
128
|
+
anc = router_kl_anchor(cur[:B, :T, :], ref[:B, :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(base.item()),
|
|
156
|
+
float(outputs.aux_loss.item()) if outputs.aux_loss is not None else 0.0,
|
|
157
|
+
la_val,
|
|
158
|
+
anc_val,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def train_epoch(self, epoch, loader, iters, max_steps=None):
|
|
162
|
+
self.model.train()
|
|
163
|
+
start_time = time.time()
|
|
164
|
+
total_steps = self.args.epochs * iters
|
|
165
|
+
for step, batch in enumerate(loader, start=1):
|
|
166
|
+
if max_steps is not None and step > max_steps:
|
|
167
|
+
break
|
|
168
|
+
loss, dpo_v, aux, la, anc = self.train_step(
|
|
169
|
+
batch, epoch * iters + step, total_steps,
|
|
170
|
+
)
|
|
171
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
172
|
+
lr = self.optimizer.param_groups[-1]["lr"]
|
|
173
|
+
Logger(
|
|
174
|
+
f"[DPO] Epoch[{epoch+1}/{self.args.epochs}]({step}/{iters}) "
|
|
175
|
+
f"loss:{loss:.4f} dpo:{dpo_v:.4f} aux:{aux:.4f} "
|
|
176
|
+
f"la:{la:.4f} anchor:{anc:.4f} lr:{lr:.2e}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _save(self, epoch, step):
|
|
180
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
181
|
+
ckp = os.path.join(self.args.save_dir, f"dpo_{self.config.hidden_size}_moe.pth")
|
|
182
|
+
self.model.eval()
|
|
183
|
+
state = self.model.state_dict()
|
|
184
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
185
|
+
self.model.train()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_dpo_loader(data_path, tokenizer, max_seq_len, batch_size):
|
|
189
|
+
ds = DPODataset(data_path, tokenizer, max_length=max_seq_len)
|
|
190
|
+
return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/grpo_trainer.py — Stage 5 GRPO trainer (self-contained).
|
|
3
|
+
|
|
4
|
+
Group Relative Policy Optimization for Chronos. Unlike minimind's
|
|
5
|
+
train_grpo.py which depends on a rollout engine + external reward model
|
|
6
|
+
+ sglang, this trainer is self-contained:
|
|
7
|
+
|
|
8
|
+
- Rollouts use ``model.generate`` (HF-style) with sampling temperature.
|
|
9
|
+
- Reward is a toy function (length + vocab purity + repetition penalty)
|
|
10
|
+
pluggable via ``reward_fn``.
|
|
11
|
+
- Reference model is a frozen deep-copy of the starting model (same
|
|
12
|
+
idea as our DPO trainer).
|
|
13
|
+
|
|
14
|
+
Loss:
|
|
15
|
+
For each prompt p, sample G completions {y_i}.
|
|
16
|
+
advantages = (r_i - mean(r_group)) / (std(r_group) + eps)
|
|
17
|
+
per_token_lp = log π_θ(y_i | p) # current policy
|
|
18
|
+
per_token_lp_ref = log π_ref(y_i | p) # stop-grad
|
|
19
|
+
kl = π_ratio^{-1}·exp(lp_ref - lp) - (lp_ref - lp) - 1 (k3)
|
|
20
|
+
L = -E[ advantage · lp_π ] + β · E[kl]
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import time
|
|
26
|
+
from contextlib import nullcontext
|
|
27
|
+
from copy import deepcopy
|
|
28
|
+
from typing import Callable, List, Tuple
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
import torch.nn.functional as F
|
|
32
|
+
from torch import optim
|
|
33
|
+
from torch.utils.data import DataLoader
|
|
34
|
+
|
|
35
|
+
import chronos.deps # noqa
|
|
36
|
+
from trainer.trainer_utils import Logger # type: ignore
|
|
37
|
+
|
|
38
|
+
from chronos.model.config import ChronosConfig
|
|
39
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
40
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
41
|
+
from chronos.trainer.optim_utils import get_lr
|
|
42
|
+
from chronos.trainer.loss_mixin import (
|
|
43
|
+
chronos_loss_term,
|
|
44
|
+
collect_router_probs,
|
|
45
|
+
router_kl_anchor,
|
|
46
|
+
capture_reference_routing,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
from chronos.trainer.reward import ToyReward, build_reward_fn # noqa: F401
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Toy vocabulary mirrors tests/fixtures/generate_tiny_alignment_data.py
|
|
54
|
+
TOY_VOCAB = {
|
|
55
|
+
"the", "quick", "brown", "fox", "machine", "learning", "model", "expert",
|
|
56
|
+
"router", "cluster", "memory", "cache", "prefetch", "latency", "throughput",
|
|
57
|
+
"tensor", "training", "inference", "decode", "weight", "gradient",
|
|
58
|
+
"softmax", "attention", "layer", "embedding", "vocab", "sequence",
|
|
59
|
+
"loss", "optimizer", "epoch", "batch", "step", "token", "logit",
|
|
60
|
+
"minimind", "chronos", "hybrid", "mixture", "of", "experts", "sparse",
|
|
61
|
+
"dense", "neural", "network", "transformer", "rope", "rotary", "kv",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def default_reward(prompt: str, response_tokens: List[int], tokenizer, target_len: int = 24) -> float:
|
|
66
|
+
"""Backward-compat wrapper; prefer chronos.trainer.reward.ToyReward."""
|
|
67
|
+
return ToyReward(target_len=target_len, vocab=TOY_VOCAB)(prompt, response_tokens, tokenizer)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ChronosGRPOTrainer:
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
model: ChronosForCausalLM,
|
|
74
|
+
config: ChronosConfig,
|
|
75
|
+
args,
|
|
76
|
+
tokenizer,
|
|
77
|
+
reward_fn: Callable = None,
|
|
78
|
+
):
|
|
79
|
+
self.model = model
|
|
80
|
+
self.config = config
|
|
81
|
+
self.args = args
|
|
82
|
+
self.tokenizer = tokenizer
|
|
83
|
+
self.device = args.device
|
|
84
|
+
self.beta = float(getattr(args, "beta", 0.04))
|
|
85
|
+
self.num_generations = int(getattr(args, "num_generations", 4))
|
|
86
|
+
self.max_gen_len = int(getattr(args, "max_gen_len", 24))
|
|
87
|
+
self.temperature = float(getattr(args, "temperature", 1.0))
|
|
88
|
+
self.reward_fn = reward_fn or default_reward
|
|
89
|
+
|
|
90
|
+
device_type = "cuda" if "cuda" in str(self.device) else "cpu"
|
|
91
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
92
|
+
self.autocast_ctx = nullcontext() if device_type == "cpu" \
|
|
93
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
94
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
95
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
96
|
+
self.optimizer = build_optimizer(
|
|
97
|
+
model, lr=args.learning_rate,
|
|
98
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.ref_model = model.__class__(config).to(self.device)
|
|
102
|
+
self.ref_model.load_state_dict(model.state_dict(), strict=False)
|
|
103
|
+
self.ref_model.eval().requires_grad_(False)
|
|
104
|
+
self._router_ref = None
|
|
105
|
+
|
|
106
|
+
def set_calibration_batch(self, x: torch.Tensor):
|
|
107
|
+
self._router_ref = capture_reference_routing(self.model, x, self.device)
|
|
108
|
+
|
|
109
|
+
# ── rollout ──────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def _rollout(self, prompt_ids: torch.Tensor) -> Tuple[torch.Tensor, List[int]]:
|
|
112
|
+
"""Sample a single completion. Returns (full_ids [1, P+L], response_len)."""
|
|
113
|
+
self.model.eval()
|
|
114
|
+
with torch.no_grad():
|
|
115
|
+
ids = prompt_ids.clone().to(self.device)
|
|
116
|
+
past = None
|
|
117
|
+
generated: List[int] = []
|
|
118
|
+
for _ in range(self.max_gen_len):
|
|
119
|
+
past_len = past[0][0].shape[1] if past else 0
|
|
120
|
+
out = self.model(
|
|
121
|
+
ids[:, past_len:] if past_len > 0 else ids,
|
|
122
|
+
past_key_values=past, use_cache=True,
|
|
123
|
+
)
|
|
124
|
+
outputs = out[0] if isinstance(out, tuple) else out
|
|
125
|
+
past = outputs.past_key_values
|
|
126
|
+
logits = outputs.logits[:, -1, :] / max(self.temperature, 1e-6)
|
|
127
|
+
probs = F.softmax(logits, dim=-1)
|
|
128
|
+
next_id = torch.multinomial(probs, num_samples=1)
|
|
129
|
+
ids = torch.cat([ids, next_id], dim=-1)
|
|
130
|
+
generated.append(int(next_id.item()))
|
|
131
|
+
if next_id.item() == self.tokenizer.eos_token_id:
|
|
132
|
+
break
|
|
133
|
+
self.model.train()
|
|
134
|
+
return ids, generated
|
|
135
|
+
|
|
136
|
+
# ── core step ────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def train_step(self, prompt_text: str, step: int, total_steps: int):
|
|
139
|
+
# Tokenize prompt
|
|
140
|
+
prompt = self.tokenizer.apply_chat_template(
|
|
141
|
+
[{"role": "user", "content": prompt_text}],
|
|
142
|
+
tokenize=False, add_generation_prompt=True,
|
|
143
|
+
)
|
|
144
|
+
prompt_ids = self.tokenizer(
|
|
145
|
+
prompt, return_tensors="pt", truncation=True, max_length=self.args.max_seq_len,
|
|
146
|
+
).input_ids.to(self.device)
|
|
147
|
+
|
|
148
|
+
# Generate G completions, compute rewards
|
|
149
|
+
trajectories: List[Tuple[torch.Tensor, int, float]] = []
|
|
150
|
+
for _ in range(self.num_generations):
|
|
151
|
+
full_ids, gen_tokens = self._rollout(prompt_ids)
|
|
152
|
+
r = self.reward_fn(prompt_text, gen_tokens, self.tokenizer)
|
|
153
|
+
trajectories.append((full_ids, len(gen_tokens), r))
|
|
154
|
+
|
|
155
|
+
rewards = torch.tensor([t[2] for t in trajectories], device=self.device)
|
|
156
|
+
adv = (rewards - rewards.mean()) / (rewards.std().clamp_min(1e-6))
|
|
157
|
+
|
|
158
|
+
# Build a padded batch of full sequences for the policy update
|
|
159
|
+
max_len = max(t[0].shape[1] for t in trajectories)
|
|
160
|
+
pad_id = self.tokenizer.pad_token_id or 0
|
|
161
|
+
ids_batch = torch.full(
|
|
162
|
+
(len(trajectories), max_len), pad_id, dtype=torch.long, device=self.device,
|
|
163
|
+
)
|
|
164
|
+
response_mask = torch.zeros(
|
|
165
|
+
(len(trajectories), max_len), dtype=torch.float, device=self.device,
|
|
166
|
+
)
|
|
167
|
+
prompt_len = prompt_ids.shape[1]
|
|
168
|
+
for i, (full, glen, _) in enumerate(trajectories):
|
|
169
|
+
L = full.shape[1]
|
|
170
|
+
ids_batch[i, :L] = full[0]
|
|
171
|
+
response_mask[i, prompt_len:L] = 1.0
|
|
172
|
+
|
|
173
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
174
|
+
for pg in self.optimizer.param_groups:
|
|
175
|
+
pg["lr"] = lr
|
|
176
|
+
|
|
177
|
+
with self.autocast_ctx:
|
|
178
|
+
outputs, lookahead_probs = self.model(ids_batch)
|
|
179
|
+
log_probs = F.log_softmax(outputs.logits[:, :-1, :], dim=-1)
|
|
180
|
+
target = ids_batch[:, 1:]
|
|
181
|
+
lp = log_probs.gather(-1, target.unsqueeze(-1)).squeeze(-1) # [G, L-1]
|
|
182
|
+
mask = response_mask[:, 1:]
|
|
183
|
+
|
|
184
|
+
with torch.no_grad():
|
|
185
|
+
ref_out = self.ref_model(ids_batch)
|
|
186
|
+
ref_outputs = ref_out[0] if isinstance(ref_out, tuple) else ref_out
|
|
187
|
+
ref_lp = F.log_softmax(ref_outputs.logits[:, :-1, :], dim=-1).gather(
|
|
188
|
+
-1, target.unsqueeze(-1)
|
|
189
|
+
).squeeze(-1)
|
|
190
|
+
|
|
191
|
+
# k3 KL estimator (Schulman 2020)
|
|
192
|
+
log_ratio = ref_lp - lp
|
|
193
|
+
kl_per_tok = (log_ratio.exp() - log_ratio - 1.0) * mask
|
|
194
|
+
kl = kl_per_tok.sum() / mask.sum().clamp_min(1.0)
|
|
195
|
+
|
|
196
|
+
# Advantage-weighted policy gradient (per-sequence)
|
|
197
|
+
seq_lp = (lp * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0)
|
|
198
|
+
pg_loss = -(adv.detach() * seq_lp).mean()
|
|
199
|
+
base = pg_loss + self.beta * kl
|
|
200
|
+
|
|
201
|
+
loss = chronos_loss_term(
|
|
202
|
+
self.model, base, lookahead_probs, self.config,
|
|
203
|
+
aux_loss=outputs.aux_loss,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
anc_val = 0.0
|
|
207
|
+
lam = float(getattr(self.config, "lambda_router_anchor", 0.0))
|
|
208
|
+
if lam > 0.0 and self._router_ref is not None:
|
|
209
|
+
r4 = collect_router_probs(self.model)
|
|
210
|
+
if r4 is not None and r4.shape[1] > 1:
|
|
211
|
+
cur = r4.mean(dim=2)
|
|
212
|
+
ref = self._router_ref
|
|
213
|
+
T = min(cur.shape[1], ref.shape[1])
|
|
214
|
+
Bm = min(cur.shape[0], ref.shape[0])
|
|
215
|
+
anc = router_kl_anchor(cur[:Bm, :T, :], ref[:Bm, :T, :], lam)
|
|
216
|
+
loss = loss + anc
|
|
217
|
+
anc_val = float(anc.item())
|
|
218
|
+
|
|
219
|
+
la_val = 0.0
|
|
220
|
+
r4 = collect_router_probs(self.model)
|
|
221
|
+
if (r4 is not None and r4.shape[1] > 1
|
|
222
|
+
and lookahead_probs is not None
|
|
223
|
+
and self.config.lookahead_steps > 0):
|
|
224
|
+
la_val = float(lookahead_supervision_loss(
|
|
225
|
+
lookahead_probs, r4.mean(dim=2).detach(),
|
|
226
|
+
self.config.lookahead_steps,
|
|
227
|
+
).item())
|
|
228
|
+
|
|
229
|
+
loss = loss / self.args.accumulation_steps
|
|
230
|
+
|
|
231
|
+
self.scaler.scale(loss).backward()
|
|
232
|
+
if step % self.args.accumulation_steps == 0:
|
|
233
|
+
self.scaler.unscale_(self.optimizer)
|
|
234
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
|
|
235
|
+
self.scaler.step(self.optimizer)
|
|
236
|
+
self.scaler.update()
|
|
237
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"loss": float(loss.item() * self.args.accumulation_steps),
|
|
241
|
+
"pg_loss": float(pg_loss.item()),
|
|
242
|
+
"kl": float(kl.item()),
|
|
243
|
+
"mean_reward": float(rewards.mean().item()),
|
|
244
|
+
"la": la_val,
|
|
245
|
+
"anchor": anc_val,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def train_epoch(self, epoch, prompts: List[str], iters, max_steps=None):
|
|
249
|
+
self.model.train()
|
|
250
|
+
total_steps = self.args.epochs * iters
|
|
251
|
+
for step, prompt in enumerate(prompts, start=1):
|
|
252
|
+
if max_steps is not None and step > max_steps:
|
|
253
|
+
break
|
|
254
|
+
if step > iters:
|
|
255
|
+
break
|
|
256
|
+
stats = self.train_step(prompt, epoch * iters + step, total_steps)
|
|
257
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
258
|
+
lr = self.optimizer.param_groups[-1]["lr"]
|
|
259
|
+
Logger(
|
|
260
|
+
f"[GRPO] Epoch[{epoch+1}/{self.args.epochs}]({step}/{iters}) "
|
|
261
|
+
f"loss:{stats['loss']:.4f} pg:{stats['pg_loss']:.4f} "
|
|
262
|
+
f"kl:{stats['kl']:.4f} r:{stats['mean_reward']:.3f} "
|
|
263
|
+
f"la:{stats['la']:.4f} anchor:{stats['anchor']:.4f} lr:{lr:.2e}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _save(self, epoch, step):
|
|
267
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
268
|
+
ckp = os.path.join(self.args.save_dir, f"grpo_{self.config.hidden_size}_moe.pth")
|
|
269
|
+
self.model.eval()
|
|
270
|
+
state = self.model.state_dict()
|
|
271
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
272
|
+
self.model.train()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def load_grpo_prompts(data_path: str, max_prompts: int = None) -> List[str]:
|
|
276
|
+
import json
|
|
277
|
+
prompts = []
|
|
278
|
+
with open(data_path) as f:
|
|
279
|
+
for line in f:
|
|
280
|
+
row = json.loads(line)
|
|
281
|
+
# Extract first user message
|
|
282
|
+
for msg in row.get("conversations", []):
|
|
283
|
+
if msg.get("role") == "user":
|
|
284
|
+
prompts.append(msg["content"])
|
|
285
|
+
break
|
|
286
|
+
if max_prompts and len(prompts) >= max_prompts:
|
|
287
|
+
break
|
|
288
|
+
return prompts
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/loss_mixin.py
|
|
3
|
+
|
|
4
|
+
Reusable loss-mixing helpers shared across all Chronos training stages
|
|
5
|
+
(pretrain, SFT, DPO, ORPO, GRPO). Centralizing this keeps each stage
|
|
6
|
+
trainer thin and guarantees lookahead supervision is preserved through
|
|
7
|
+
the whole 6-stage chain rather than being silently dropped by a stage
|
|
8
|
+
that forgot to add it.
|
|
9
|
+
|
|
10
|
+
Two helpers are exposed:
|
|
11
|
+
|
|
12
|
+
- ``chronos_loss_term(model, base_loss, lookahead_probs, config)`` —
|
|
13
|
+
appends λ_balance·L_aux + λ_temporal·L_temporal + λ_lookahead·L_lookahead
|
|
14
|
+
to any base loss (CE for pretrain/SFT, DPO loss, ORPO loss, etc.).
|
|
15
|
+
|
|
16
|
+
- ``router_kl_anchor(current_router_probs, ref_router_probs, lambda_anchor)`` —
|
|
17
|
+
KL(current || reference) penalty. Captured at the start of an alignment
|
|
18
|
+
stage by running the freshly-loaded model in eval mode on a calibration
|
|
19
|
+
batch; prevents the alignment gradients from drifting routing away from
|
|
20
|
+
the pretrained distribution that the cluster layout was optimized for.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import List, Optional
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
29
|
+
from chronos.model.temporal_loss import (
|
|
30
|
+
temporal_locality_loss,
|
|
31
|
+
lookahead_supervision_loss,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def collect_router_probs(model, *, with_grad: bool = False) -> Optional[torch.Tensor]:
|
|
36
|
+
"""Stack router probs from every ChronosMOEFeedForward layer.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
with_grad: when True, returns the live tensor that still has an
|
|
40
|
+
autograd edge back to the gate (used by training loss
|
|
41
|
+
terms that need to shape the gate). When False, returns
|
|
42
|
+
the detached tensor (safe for logging / cluster layout
|
|
43
|
+
/ KL anchor reference / lookahead teacher).
|
|
44
|
+
|
|
45
|
+
Returns: ``[B, S, L, E]`` or ``None`` if no MoE layers fired in the
|
|
46
|
+
last forward pass (e.g. before the first forward).
|
|
47
|
+
"""
|
|
48
|
+
probs: List[torch.Tensor] = []
|
|
49
|
+
for layer in model.model.layers:
|
|
50
|
+
mlp = getattr(layer, "mlp", None)
|
|
51
|
+
if isinstance(mlp, ChronosMOEFeedForward):
|
|
52
|
+
p = mlp.last_router_probs_grad if with_grad else mlp.last_router_probs
|
|
53
|
+
if p is not None:
|
|
54
|
+
probs.append(p)
|
|
55
|
+
if not probs:
|
|
56
|
+
return None
|
|
57
|
+
return torch.stack(probs, dim=2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def chronos_loss_term(
|
|
61
|
+
model,
|
|
62
|
+
base_loss: torch.Tensor,
|
|
63
|
+
lookahead_probs: Optional[torch.Tensor],
|
|
64
|
+
config,
|
|
65
|
+
*,
|
|
66
|
+
aux_loss: Optional[torch.Tensor] = None,
|
|
67
|
+
) -> torch.Tensor:
|
|
68
|
+
"""
|
|
69
|
+
Combine `base_loss` with the Chronos-specific regularizers:
|
|
70
|
+
|
|
71
|
+
L = L_base
|
|
72
|
+
+ λ_balance · L_aux (if aux_loss provided)
|
|
73
|
+
+ λ_temporal · L_temporal (if seq has ≥2 tokens)
|
|
74
|
+
+ λ_lookahead · L_lookahead (if lookahead_probs + λ_la > 0)
|
|
75
|
+
|
|
76
|
+
The function is safe to call from any stage: it gracefully degrades
|
|
77
|
+
when the necessary tensors are unavailable (e.g. lookahead_probs is
|
|
78
|
+
None during a model that doesn't expose it).
|
|
79
|
+
"""
|
|
80
|
+
loss = base_loss
|
|
81
|
+
|
|
82
|
+
if aux_loss is not None:
|
|
83
|
+
lambda_balance = float(getattr(config, "lambda_balance", 0.0))
|
|
84
|
+
if lambda_balance > 0.0:
|
|
85
|
+
# Prefer the un-scaled raw aux summed across MoE layers so we
|
|
86
|
+
# don't double-multiply by router_aux_loss_coef. Falls back to
|
|
87
|
+
# whatever the model handed us if aux_loss_raw isn't populated
|
|
88
|
+
# (e.g. eval-mode forward).
|
|
89
|
+
raw_total = base_loss.new_zeros(())
|
|
90
|
+
saw_raw = False
|
|
91
|
+
for layer in model.model.layers:
|
|
92
|
+
mlp = getattr(layer, "mlp", None)
|
|
93
|
+
if isinstance(mlp, ChronosMOEFeedForward) and mlp.aux_loss_raw is not None:
|
|
94
|
+
raw_total = raw_total + mlp.aux_loss_raw
|
|
95
|
+
saw_raw = True
|
|
96
|
+
aux_term = raw_total if saw_raw else aux_loss
|
|
97
|
+
loss = loss + lambda_balance * aux_term
|
|
98
|
+
|
|
99
|
+
router_4d = collect_router_probs(model, with_grad=True)
|
|
100
|
+
if router_4d is not None and router_4d.shape[1] > 1:
|
|
101
|
+
router_mean = router_4d.mean(dim=2) # [B, S, E]
|
|
102
|
+
lambda_temporal = float(getattr(config, "lambda_temporal", 0.0))
|
|
103
|
+
if lambda_temporal > 0.0:
|
|
104
|
+
loss = loss + lambda_temporal * temporal_locality_loss(router_mean)
|
|
105
|
+
|
|
106
|
+
lambda_lookahead = float(getattr(config, "lambda_lookahead", 0.0))
|
|
107
|
+
lookahead_steps = int(getattr(config, "lookahead_steps", 0))
|
|
108
|
+
if (
|
|
109
|
+
lookahead_probs is not None
|
|
110
|
+
and lambda_lookahead > 0.0
|
|
111
|
+
and lookahead_steps > 0
|
|
112
|
+
):
|
|
113
|
+
# Teacher must be detached: lookahead targets are stop-grad'd
|
|
114
|
+
# by construction (we predict the *future* router state).
|
|
115
|
+
teacher = router_mean.detach()
|
|
116
|
+
la = lookahead_supervision_loss(lookahead_probs, teacher, lookahead_steps)
|
|
117
|
+
loss = loss + lambda_lookahead * la
|
|
118
|
+
return loss
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def router_kl_anchor(
|
|
122
|
+
current_router_probs: torch.Tensor,
|
|
123
|
+
ref_router_probs: torch.Tensor,
|
|
124
|
+
lambda_anchor: float,
|
|
125
|
+
) -> torch.Tensor:
|
|
126
|
+
"""KL(current || reference) over routing distributions, scaled by λ.
|
|
127
|
+
|
|
128
|
+
Both inputs are layer-averaged ``[B, S, E]`` distributions (post-softmax).
|
|
129
|
+
Reference is treated as a fixed teacher (no grad needed; caller passes
|
|
130
|
+
a detached tensor). Reduces to mean over ``B*S``.
|
|
131
|
+
"""
|
|
132
|
+
if lambda_anchor <= 0.0:
|
|
133
|
+
return current_router_probs.new_zeros(())
|
|
134
|
+
eps = 1e-9
|
|
135
|
+
kl = (current_router_probs * (
|
|
136
|
+
current_router_probs.clamp_min(eps).log()
|
|
137
|
+
- ref_router_probs.clamp_min(eps).log()
|
|
138
|
+
)).sum(dim=-1).mean()
|
|
139
|
+
return lambda_anchor * kl
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def capture_reference_routing(model, calibration_batch, device) -> torch.Tensor:
|
|
143
|
+
"""Run `model` in eval mode on the calibration batch, return the
|
|
144
|
+
layer-averaged routing distribution as a detached reference for the
|
|
145
|
+
`router_kl_anchor` term.
|
|
146
|
+
|
|
147
|
+
The calibration batch is a single ``input_ids`` tensor (already on
|
|
148
|
+
device-or-not is handled here). Returns ``[B, S, E]`` detached.
|
|
149
|
+
"""
|
|
150
|
+
was_training = model.training
|
|
151
|
+
model.eval()
|
|
152
|
+
with torch.no_grad():
|
|
153
|
+
ids = calibration_batch.to(device)
|
|
154
|
+
try:
|
|
155
|
+
_ = model(ids, use_cache=False)
|
|
156
|
+
except TypeError:
|
|
157
|
+
_ = model(ids) # minimind models don't accept use_cache kwarg here
|
|
158
|
+
router_4d = collect_router_probs(model)
|
|
159
|
+
if router_4d is None:
|
|
160
|
+
ref = None
|
|
161
|
+
else:
|
|
162
|
+
ref = router_4d.mean(dim=2).detach()
|
|
163
|
+
if was_training:
|
|
164
|
+
model.train()
|
|
165
|
+
return ref
|