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.
Files changed (83) hide show
  1. chronos/__init__.py +16 -0
  2. chronos/_backend_legacy.py +64 -0
  3. chronos/app.py +122 -0
  4. chronos/backend/__init__.py +40 -0
  5. chronos/backend/dispatcher.py +347 -0
  6. chronos/backend/ext/__init__.py +2 -0
  7. chronos/backend/ext/opencl.py +22 -0
  8. chronos/cli.py +62 -0
  9. chronos/data/__init__.py +0 -0
  10. chronos/data/flexible_dataset.py +309 -0
  11. chronos/deps.py +72 -0
  12. chronos/eval/__init__.py +2 -0
  13. chronos/eval/benchmark.py +159 -0
  14. chronos/eval/io_profiler.py +232 -0
  15. chronos/io/__init__.py +13 -0
  16. chronos/io/async_prefetcher.py +220 -0
  17. chronos/io/cluster_layout.py +317 -0
  18. chronos/io/expert_store.py +559 -0
  19. chronos/io/io_simulator.py +40 -0
  20. chronos/io/storage.py +237 -0
  21. chronos/mlx/__init__.py +32 -0
  22. chronos/mlx/attention.py +230 -0
  23. chronos/mlx/expert_store.py +155 -0
  24. chronos/mlx/inference.py +159 -0
  25. chronos/mlx/model.py +181 -0
  26. chronos/mlx/moe.py +102 -0
  27. chronos/model/__init__.py +6 -0
  28. chronos/model/config.py +54 -0
  29. chronos/model/hf_io.py +149 -0
  30. chronos/model/hybrid_attention.py +237 -0
  31. chronos/model/lookahead_router.py +55 -0
  32. chronos/model/model_chronos.py +271 -0
  33. chronos/model/moe_chronos.py +189 -0
  34. chronos/model/temporal_loss.py +124 -0
  35. chronos/router/__init__.py +12 -0
  36. chronos/router/expert_predictor.py +163 -0
  37. chronos/router/intent_classifier.py +193 -0
  38. chronos/router/prefill_scheduler.py +248 -0
  39. chronos/runtime/__init__.py +2 -0
  40. chronos/runtime/cache_manager.py +117 -0
  41. chronos/runtime/inference_engine.py +295 -0
  42. chronos/runtime/metrics.py +75 -0
  43. chronos/serving/__init__.py +13 -0
  44. chronos/serving/vllm_adapter.py +85 -0
  45. chronos/trainer/__init__.py +6 -0
  46. chronos/trainer/chronos_trainer.py +151 -0
  47. chronos/trainer/distill_trainer.py +193 -0
  48. chronos/trainer/dpo_trainer.py +190 -0
  49. chronos/trainer/grpo_trainer.py +288 -0
  50. chronos/trainer/loss_mixin.py +165 -0
  51. chronos/trainer/optim_utils.py +101 -0
  52. chronos/trainer/orpo_trainer.py +189 -0
  53. chronos/trainer/reward.py +130 -0
  54. chronos/trainer/sft_trainer.py +189 -0
  55. chronos/tuning/__init__.py +1 -0
  56. chronos/tuning/_base_tuner.py +284 -0
  57. chronos/tuning/chronos_auto_tuner.py +203 -0
  58. project_chronos-0.1.1.dist-info/METADATA +670 -0
  59. project_chronos-0.1.1.dist-info/RECORD +83 -0
  60. project_chronos-0.1.1.dist-info/WHEEL +5 -0
  61. project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
  62. project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
  63. project_chronos-0.1.1.dist-info/top_level.txt +8 -0
  64. train_chronos.py +133 -0
  65. train_chronos_distill.py +95 -0
  66. train_chronos_dpo.py +82 -0
  67. train_chronos_grpo.py +88 -0
  68. train_chronos_orpo.py +81 -0
  69. train_chronos_sft.py +85 -0
  70. ui/__init__.py +3 -0
  71. ui/estimator.py +204 -0
  72. ui/gradio_compat.py +72 -0
  73. ui/i18n.py +540 -0
  74. ui/presets.py +194 -0
  75. ui/tabs/__init__.py +3 -0
  76. ui/tabs/autotune_tab.py +210 -0
  77. ui/tabs/benchmark_tab.py +196 -0
  78. ui/tabs/config_tab.py +318 -0
  79. ui/tabs/data_tab.py +148 -0
  80. ui/tabs/inference_tab.py +496 -0
  81. ui/tabs/iomon_tab.py +37 -0
  82. ui/tabs/pipeline_tab.py +134 -0
  83. 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