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,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/runtime/metrics.py
|
|
3
|
+
|
|
4
|
+
A tiny in-process metrics bus. ExpertStore and AsyncPrefetcher push events
|
|
5
|
+
into the singleton bus; the WebUI's IO Monitor tab polls ``snapshot()`` and
|
|
6
|
+
plots the rolling buffers.
|
|
7
|
+
|
|
8
|
+
Design constraints:
|
|
9
|
+
|
|
10
|
+
- **Zero deps** — pure stdlib. No Prometheus, no opentelemetry.
|
|
11
|
+
- **Thread-safe** — both the prefetcher daemon and the main thread emit.
|
|
12
|
+
- **Bounded memory** — each metric is a deque(maxlen=600) so a 1-Hz UI
|
|
13
|
+
poll over a 10-minute window stays under a few KB.
|
|
14
|
+
- **Optional** — emitting is wrapped in a try/except so missing
|
|
15
|
+
``chronos.runtime.metrics`` cannot crash the inference loop.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from collections import deque
|
|
22
|
+
from typing import Deque, Dict, List, Tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_BUFFER_SIZE = 600 # ~10 min at 1 Hz
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MetricsBus:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self._lock = threading.Lock()
|
|
31
|
+
# metric_name -> deque[(timestamp, value)]
|
|
32
|
+
self._buffers: Dict[str, Deque[Tuple[float, float]]] = {}
|
|
33
|
+
|
|
34
|
+
def record(self, name: str, value: float, ts: float = None) -> None:
|
|
35
|
+
if ts is None:
|
|
36
|
+
ts = time.monotonic()
|
|
37
|
+
with self._lock:
|
|
38
|
+
buf = self._buffers.get(name)
|
|
39
|
+
if buf is None:
|
|
40
|
+
buf = deque(maxlen=_BUFFER_SIZE)
|
|
41
|
+
self._buffers[name] = buf
|
|
42
|
+
buf.append((float(ts), float(value)))
|
|
43
|
+
|
|
44
|
+
def latest(self, name: str) -> float:
|
|
45
|
+
with self._lock:
|
|
46
|
+
buf = self._buffers.get(name)
|
|
47
|
+
if not buf:
|
|
48
|
+
return float("nan")
|
|
49
|
+
return buf[-1][1]
|
|
50
|
+
|
|
51
|
+
def series(self, name: str) -> List[Tuple[float, float]]:
|
|
52
|
+
with self._lock:
|
|
53
|
+
buf = self._buffers.get(name)
|
|
54
|
+
return list(buf) if buf else []
|
|
55
|
+
|
|
56
|
+
def snapshot(self) -> Dict[str, List[Tuple[float, float]]]:
|
|
57
|
+
with self._lock:
|
|
58
|
+
return {k: list(v) for k, v in self._buffers.items()}
|
|
59
|
+
|
|
60
|
+
def reset(self) -> None:
|
|
61
|
+
with self._lock:
|
|
62
|
+
self._buffers.clear()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Module-level singleton. Importing this module is essentially free.
|
|
66
|
+
bus = MetricsBus()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def safe_record(name: str, value: float) -> None:
|
|
70
|
+
"""Convenience wrapper that swallows any exception so metrics emission
|
|
71
|
+
can never break the hot path."""
|
|
72
|
+
try:
|
|
73
|
+
bus.record(name, value)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/serving/vllm_adapter.py
|
|
3
|
+
|
|
4
|
+
Optional vLLM serving adapter. ``vllm`` is NOT a requirement of Chronos
|
|
5
|
+
and the module does the right thing whether or not it's installed.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from chronos.serving import register_chronos_with_vllm
|
|
10
|
+
register_chronos_with_vllm() # no-op if vllm not installed
|
|
11
|
+
|
|
12
|
+
When ``vllm`` is available, ``ChronosForCausalLM`` is registered in
|
|
13
|
+
``vllm.ModelRegistry`` so vLLM can find the architecture by name. The
|
|
14
|
+
actual worker-side glue (paged KV cache interop, per-layer expert mask
|
|
15
|
+
hook) is version-sensitive; see ``docs/vllm_integration.md`` for the
|
|
16
|
+
full integration steps and known gotchas.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib.util
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
HAS_VLLM = importlib.util.find_spec("vllm") is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register_chronos_with_vllm(verbose: bool = True) -> bool:
|
|
28
|
+
"""Register ``ChronosForCausalLM`` with vLLM's ModelRegistry.
|
|
29
|
+
|
|
30
|
+
Returns True on success, False if vLLM is not installed or the API
|
|
31
|
+
shape has changed. This function is idempotent.
|
|
32
|
+
"""
|
|
33
|
+
if not HAS_VLLM:
|
|
34
|
+
if verbose:
|
|
35
|
+
print(
|
|
36
|
+
"[chronos.serving] vllm not installed — skipping registration.\n"
|
|
37
|
+
" To enable vLLM serving: `pip install vllm`\n"
|
|
38
|
+
" (Linux + CUDA required; see docs/vllm_integration.md)"
|
|
39
|
+
)
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import vllm # noqa: F401
|
|
44
|
+
from vllm import ModelRegistry
|
|
45
|
+
|
|
46
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
47
|
+
|
|
48
|
+
# ModelRegistry.register_model signature changes across vLLM
|
|
49
|
+
# versions. Try the common shapes.
|
|
50
|
+
arch = "ChronosForCausalLM"
|
|
51
|
+
if hasattr(ModelRegistry, "register_model"):
|
|
52
|
+
try:
|
|
53
|
+
ModelRegistry.register_model(arch, ChronosForCausalLM)
|
|
54
|
+
except TypeError:
|
|
55
|
+
# Older API: register_model(name, module_path, class_name)
|
|
56
|
+
ModelRegistry.register_model(
|
|
57
|
+
arch, "chronos.model.model_chronos", "ChronosForCausalLM",
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
if verbose:
|
|
61
|
+
print("[chronos.serving] vllm.ModelRegistry API not recognized; skipping.")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
if verbose:
|
|
65
|
+
print(f"[chronos.serving] Registered {arch} with vLLM ModelRegistry.")
|
|
66
|
+
return True
|
|
67
|
+
except Exception as e:
|
|
68
|
+
if verbose:
|
|
69
|
+
print(f"[chronos.serving] vLLM registration failed: {e}")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_available_expert_masks(model, masks) -> None:
|
|
74
|
+
"""Hook point for vLLM worker to inject per-step expert availability
|
|
75
|
+
masks. Sets ``model._chronos_available_expert_masks`` so the next
|
|
76
|
+
forward picks it up (if the model is wired to check there).
|
|
77
|
+
|
|
78
|
+
This is a best-effort placeholder; the actual decode-loop glue depends
|
|
79
|
+
on the vLLM version and is documented in docs/vllm_integration.md.
|
|
80
|
+
"""
|
|
81
|
+
setattr(model, "_chronos_available_expert_masks", masks)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_available() -> bool:
|
|
85
|
+
return HAS_VLLM
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .chronos_trainer import ChronosTrainer
|
|
2
|
+
from .sft_trainer import ChronosSFTTrainer
|
|
3
|
+
from .dpo_trainer import ChronosDPOTrainer
|
|
4
|
+
from .orpo_trainer import ChronosORPOTrainer
|
|
5
|
+
from .grpo_trainer import ChronosGRPOTrainer
|
|
6
|
+
from .distill_trainer import ChronosDistillTrainer
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import chronos.deps # ensure minimind on sys.path
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import torch
|
|
7
|
+
import torch.nn.functional as F
|
|
8
|
+
from torch import optim
|
|
9
|
+
from torch.utils.data import DataLoader
|
|
10
|
+
from contextlib import nullcontext
|
|
11
|
+
|
|
12
|
+
from model.model_minimind import MiniMindConfig
|
|
13
|
+
from trainer.trainer_utils import (
|
|
14
|
+
Logger, is_main_process, lm_checkpoint, setup_seed,
|
|
15
|
+
)
|
|
16
|
+
from chronos.trainer.optim_utils import get_lr # warmup→cosine override
|
|
17
|
+
from chronos.data.flexible_dataset import (
|
|
18
|
+
FlexibleDataset as PretrainDataset,
|
|
19
|
+
StreamingSFTDataset as SFTDataset,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from chronos.model.config import ChronosConfig
|
|
23
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
24
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
25
|
+
from chronos.trainer.loss_mixin import chronos_loss_term, collect_router_probs
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChronosTrainer:
|
|
29
|
+
"""
|
|
30
|
+
Training orchestrator for Project Chronos.
|
|
31
|
+
|
|
32
|
+
Wraps minimind's training loop and injects:
|
|
33
|
+
- temporal_locality_loss via λ2 * ||E_t - E_{t-1}||^2
|
|
34
|
+
- load_balance_loss via λ1 (already computed inside ChronosMOEFeedForward)
|
|
35
|
+
- LookaheadRouter accuracy logging
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: ChronosConfig, args):
|
|
39
|
+
self.config = config
|
|
40
|
+
self.args = args
|
|
41
|
+
self.device = args.device
|
|
42
|
+
seed = getattr(args, "seed", None)
|
|
43
|
+
if seed is not None:
|
|
44
|
+
setup_seed(int(seed))
|
|
45
|
+
device_type = "cuda" if "cuda" in self.device else "cpu"
|
|
46
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
47
|
+
self.autocast_ctx = (
|
|
48
|
+
nullcontext() if device_type == "cpu"
|
|
49
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
50
|
+
)
|
|
51
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
52
|
+
|
|
53
|
+
self.model = ChronosForCausalLM(config).to(self.device)
|
|
54
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
55
|
+
self.optimizer = build_optimizer(
|
|
56
|
+
self.model, lr=args.learning_rate,
|
|
57
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _collect_router_probs(self) -> torch.Tensor:
|
|
61
|
+
"""Gather last_router_probs from all MoE layers → [B, S, L, E]."""
|
|
62
|
+
return collect_router_probs(self.model)
|
|
63
|
+
|
|
64
|
+
def train_step(self, input_ids, labels, step, total_steps):
|
|
65
|
+
input_ids = input_ids.to(self.device)
|
|
66
|
+
labels = labels.to(self.device)
|
|
67
|
+
|
|
68
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
69
|
+
for pg in self.optimizer.param_groups:
|
|
70
|
+
pg['lr'] = lr
|
|
71
|
+
|
|
72
|
+
with self.autocast_ctx:
|
|
73
|
+
outputs, lookahead_probs = self.model(input_ids, labels=labels)
|
|
74
|
+
ce_loss = outputs.loss
|
|
75
|
+
aux_loss = outputs.aux_loss
|
|
76
|
+
|
|
77
|
+
loss = chronos_loss_term(
|
|
78
|
+
self.model, ce_loss, lookahead_probs, self.config,
|
|
79
|
+
aux_loss=aux_loss,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Standalone lookahead loss value for logging
|
|
83
|
+
lookahead_loss_val = 0.0
|
|
84
|
+
router_4d = self._collect_router_probs()
|
|
85
|
+
if (
|
|
86
|
+
router_4d is not None
|
|
87
|
+
and router_4d.shape[1] > 1
|
|
88
|
+
and lookahead_probs is not None
|
|
89
|
+
and self.config.lookahead_steps > 0
|
|
90
|
+
):
|
|
91
|
+
teacher = router_4d.mean(dim=2).detach()
|
|
92
|
+
la_term = lookahead_supervision_loss(
|
|
93
|
+
lookahead_probs, teacher, self.config.lookahead_steps,
|
|
94
|
+
)
|
|
95
|
+
lookahead_loss_val = float(la_term.item())
|
|
96
|
+
|
|
97
|
+
loss = loss / self.args.accumulation_steps
|
|
98
|
+
|
|
99
|
+
self.scaler.scale(loss).backward()
|
|
100
|
+
|
|
101
|
+
if step % self.args.accumulation_steps == 0:
|
|
102
|
+
self.scaler.unscale_(self.optimizer)
|
|
103
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
|
|
104
|
+
self.scaler.step(self.optimizer)
|
|
105
|
+
self.scaler.update()
|
|
106
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
107
|
+
|
|
108
|
+
return loss.item() * self.args.accumulation_steps, ce_loss.item(), aux_loss.item(), lookahead_loss_val
|
|
109
|
+
|
|
110
|
+
def train_epoch(self, epoch, loader, iters, start_step=0, wandb=None):
|
|
111
|
+
self.model.train()
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
total_steps = self.args.epochs * iters
|
|
114
|
+
|
|
115
|
+
for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):
|
|
116
|
+
loss, ce_loss, aux_loss, la_loss = self.train_step(
|
|
117
|
+
input_ids, labels, epoch * iters + step, total_steps
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
121
|
+
elapsed = time.time() - start_time
|
|
122
|
+
eta = elapsed / max(step - start_step, 1) * (iters - step) // 60
|
|
123
|
+
lr = self.optimizer.param_groups[-1]['lr']
|
|
124
|
+
Logger(
|
|
125
|
+
f'Epoch:[{epoch+1}/{self.args.epochs}]({step}/{iters}) '
|
|
126
|
+
f'loss:{loss:.4f} ce:{ce_loss:.4f} aux:{aux_loss:.4f} '
|
|
127
|
+
f'la:{la_loss:.4f} lr:{lr:.2e} eta:{eta:.1f}min'
|
|
128
|
+
)
|
|
129
|
+
if wandb:
|
|
130
|
+
wandb.log({
|
|
131
|
+
"loss": loss, "ce_loss": ce_loss, "aux_loss": aux_loss,
|
|
132
|
+
"lookahead_loss": la_loss, "lr": lr,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (step % self.args.save_interval == 0 or step == iters) and is_main_process():
|
|
136
|
+
self._save(epoch, step, wandb)
|
|
137
|
+
|
|
138
|
+
def _save(self, epoch, step, wandb=None):
|
|
139
|
+
self.model.eval()
|
|
140
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
141
|
+
ckp = f'{self.args.save_dir}/chronos_{self.config.hidden_size}_moe.pth'
|
|
142
|
+
state = self.model.state_dict()
|
|
143
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
144
|
+
lm_checkpoint(
|
|
145
|
+
self.config, weight='chronos', model=self.model,
|
|
146
|
+
optimizer=self.optimizer, scaler=self.scaler,
|
|
147
|
+
epoch=epoch, step=step, wandb=wandb,
|
|
148
|
+
save_dir=self.args.save_dir,
|
|
149
|
+
)
|
|
150
|
+
self.model.train()
|
|
151
|
+
del state
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/trainer/distill_trainer.py — Stage 6 distillation trainer.
|
|
3
|
+
|
|
4
|
+
Loss:
|
|
5
|
+
L_KD = T² · KL( softmax(student_logits/T) || softmax(teacher_logits/T) )
|
|
6
|
+
L_label = cross_entropy(student_logits, labels, ignore_index=-100)
|
|
7
|
+
L_total = α · L_KD + (1 − α) · L_label + chronos_loss_term (lookahead etc.)
|
|
8
|
+
|
|
9
|
+
The teacher is any frozen model that returns ``.logits`` on forward (Chronos
|
|
10
|
+
or MiniMind checkpoints both work). Router KL anchor still applies — Stage
|
|
11
|
+
6 shouldn't corrupt the cache layout any more than SFT/DPO can.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from contextlib import nullcontext
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn.functional as F
|
|
21
|
+
from torch import optim
|
|
22
|
+
from torch.utils.data import DataLoader
|
|
23
|
+
|
|
24
|
+
import chronos.deps # noqa: F401
|
|
25
|
+
from trainer.trainer_utils import Logger # type: ignore
|
|
26
|
+
from chronos.data.flexible_dataset import StreamingSFTDataset as SFTDataset
|
|
27
|
+
|
|
28
|
+
from chronos.model.config import ChronosConfig
|
|
29
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
30
|
+
from chronos.model.temporal_loss import lookahead_supervision_loss
|
|
31
|
+
from chronos.trainer.optim_utils import get_lr
|
|
32
|
+
from chronos.trainer.loss_mixin import (
|
|
33
|
+
chronos_loss_term,
|
|
34
|
+
collect_router_probs,
|
|
35
|
+
router_kl_anchor,
|
|
36
|
+
capture_reference_routing,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def distillation_kl(student_logits: torch.Tensor, teacher_logits: torch.Tensor,
|
|
41
|
+
T: float = 4.0, ignore_mask: torch.Tensor = None) -> torch.Tensor:
|
|
42
|
+
"""Temperature-scaled KL divergence; caller is responsible for masking."""
|
|
43
|
+
s = F.log_softmax(student_logits / T, dim=-1)
|
|
44
|
+
t = F.softmax(teacher_logits / T, dim=-1).detach()
|
|
45
|
+
kl = F.kl_div(s, t, reduction="none").sum(dim=-1) # [B, S]
|
|
46
|
+
if ignore_mask is not None:
|
|
47
|
+
kl = kl * ignore_mask
|
|
48
|
+
denom = ignore_mask.sum().clamp_min(1.0)
|
|
49
|
+
else:
|
|
50
|
+
denom = torch.tensor(kl.numel(), dtype=kl.dtype, device=kl.device).clamp_min(1.0)
|
|
51
|
+
return (T * T) * (kl.sum() / denom)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ChronosDistillTrainer:
|
|
55
|
+
def __init__(self, model: ChronosForCausalLM, teacher, config: ChronosConfig,
|
|
56
|
+
args, tokenizer):
|
|
57
|
+
self.model = model
|
|
58
|
+
self.teacher = teacher.eval().requires_grad_(False)
|
|
59
|
+
self.config = config
|
|
60
|
+
self.args = args
|
|
61
|
+
self.tokenizer = tokenizer
|
|
62
|
+
self.device = args.device
|
|
63
|
+
self.alpha = float(getattr(args, "alpha", 0.7))
|
|
64
|
+
self.temperature = float(getattr(args, "temperature", 4.0))
|
|
65
|
+
|
|
66
|
+
device_type = "cuda" if "cuda" in str(self.device) else "cpu"
|
|
67
|
+
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
|
68
|
+
self.autocast_ctx = nullcontext() if device_type == "cpu" \
|
|
69
|
+
else torch.cuda.amp.autocast(dtype=dtype)
|
|
70
|
+
self.scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == "float16"))
|
|
71
|
+
from chronos.trainer.optim_utils import build_optimizer
|
|
72
|
+
self.optimizer = build_optimizer(
|
|
73
|
+
model, lr=args.learning_rate,
|
|
74
|
+
weight_decay=float(getattr(args, "weight_decay", 0.01)),
|
|
75
|
+
)
|
|
76
|
+
self._router_ref = None
|
|
77
|
+
|
|
78
|
+
def set_calibration_batch(self, x: torch.Tensor):
|
|
79
|
+
self._router_ref = capture_reference_routing(self.model, x, self.device)
|
|
80
|
+
|
|
81
|
+
def _teacher_logits(self, input_ids: torch.Tensor) -> torch.Tensor:
|
|
82
|
+
with torch.no_grad():
|
|
83
|
+
out = self.teacher(input_ids)
|
|
84
|
+
return (out[0].logits if isinstance(out, tuple) else out.logits).detach()
|
|
85
|
+
|
|
86
|
+
def train_step(self, input_ids, labels, step, total_steps):
|
|
87
|
+
input_ids = input_ids.to(self.device)
|
|
88
|
+
labels = labels.to(self.device)
|
|
89
|
+
|
|
90
|
+
lr = get_lr(step, total_steps, self.args.learning_rate)
|
|
91
|
+
for pg in self.optimizer.param_groups:
|
|
92
|
+
pg["lr"] = lr
|
|
93
|
+
|
|
94
|
+
with self.autocast_ctx:
|
|
95
|
+
outputs, lookahead_probs = self.model(input_ids, labels=labels)
|
|
96
|
+
ce_loss = outputs.loss
|
|
97
|
+
student_logits = outputs.logits
|
|
98
|
+
|
|
99
|
+
# Teacher forward (no grad)
|
|
100
|
+
teacher_logits = self._teacher_logits(input_ids)
|
|
101
|
+
# Align vocab dims if teacher is smaller (pad or truncate)
|
|
102
|
+
if teacher_logits.shape[-1] != student_logits.shape[-1]:
|
|
103
|
+
V = min(teacher_logits.shape[-1], student_logits.shape[-1])
|
|
104
|
+
teacher_logits = teacher_logits[..., :V]
|
|
105
|
+
student_logits_kd = student_logits[..., :V]
|
|
106
|
+
else:
|
|
107
|
+
student_logits_kd = student_logits
|
|
108
|
+
# Align seq len
|
|
109
|
+
S = min(teacher_logits.shape[1], student_logits_kd.shape[1])
|
|
110
|
+
teacher_logits = teacher_logits[:, :S, :]
|
|
111
|
+
student_logits_kd = student_logits_kd[:, :S, :]
|
|
112
|
+
|
|
113
|
+
# Loss mask = positions where label != -100
|
|
114
|
+
ignore_mask = (labels[:, :S] != -100).float() if labels.shape[1] >= S else None
|
|
115
|
+
kd = distillation_kl(
|
|
116
|
+
student_logits_kd, teacher_logits,
|
|
117
|
+
T=self.temperature, ignore_mask=ignore_mask,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
base = self.alpha * kd + (1.0 - self.alpha) * ce_loss
|
|
121
|
+
loss = chronos_loss_term(
|
|
122
|
+
self.model, base, lookahead_probs, self.config,
|
|
123
|
+
aux_loss=outputs.aux_loss,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
anc_val = 0.0
|
|
127
|
+
lam = float(getattr(self.config, "lambda_router_anchor", 0.0))
|
|
128
|
+
if lam > 0.0 and self._router_ref is not None:
|
|
129
|
+
r4 = collect_router_probs(self.model)
|
|
130
|
+
if r4 is not None and r4.shape[1] > 1:
|
|
131
|
+
cur = r4.mean(dim=2)
|
|
132
|
+
ref = self._router_ref
|
|
133
|
+
T_ax = min(cur.shape[1], ref.shape[1])
|
|
134
|
+
B = min(cur.shape[0], ref.shape[0])
|
|
135
|
+
anc = router_kl_anchor(cur[:B, :T_ax, :], ref[:B, :T_ax, :], lam)
|
|
136
|
+
loss = loss + anc
|
|
137
|
+
anc_val = float(anc.item())
|
|
138
|
+
|
|
139
|
+
la_val = 0.0
|
|
140
|
+
r4 = collect_router_probs(self.model)
|
|
141
|
+
if (r4 is not None and r4.shape[1] > 1
|
|
142
|
+
and lookahead_probs is not None
|
|
143
|
+
and self.config.lookahead_steps > 0):
|
|
144
|
+
la_val = float(lookahead_supervision_loss(
|
|
145
|
+
lookahead_probs, r4.mean(dim=2).detach(),
|
|
146
|
+
self.config.lookahead_steps,
|
|
147
|
+
).item())
|
|
148
|
+
|
|
149
|
+
loss = loss / self.args.accumulation_steps
|
|
150
|
+
|
|
151
|
+
self.scaler.scale(loss).backward()
|
|
152
|
+
if step % self.args.accumulation_steps == 0:
|
|
153
|
+
self.scaler.unscale_(self.optimizer)
|
|
154
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
|
|
155
|
+
self.scaler.step(self.optimizer)
|
|
156
|
+
self.scaler.update()
|
|
157
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"loss": float(loss.item() * self.args.accumulation_steps),
|
|
161
|
+
"kd": float(kd.item()),
|
|
162
|
+
"ce": float(ce_loss.item()),
|
|
163
|
+
"la": la_val,
|
|
164
|
+
"anchor": anc_val,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def train_epoch(self, epoch, loader, iters, max_steps=None):
|
|
168
|
+
self.model.train()
|
|
169
|
+
total_steps = self.args.epochs * iters
|
|
170
|
+
for step, (ids, labels) in enumerate(loader, start=1):
|
|
171
|
+
if max_steps is not None and step > max_steps:
|
|
172
|
+
break
|
|
173
|
+
s = self.train_step(ids, labels, epoch * iters + step, total_steps)
|
|
174
|
+
if step % self.args.log_interval == 0 or step == iters:
|
|
175
|
+
lr = self.optimizer.param_groups[-1]["lr"]
|
|
176
|
+
Logger(
|
|
177
|
+
f"[Distill] Epoch[{epoch+1}/{self.args.epochs}]({step}/{iters}) "
|
|
178
|
+
f"loss:{s['loss']:.4f} kd:{s['kd']:.4f} ce:{s['ce']:.4f} "
|
|
179
|
+
f"la:{s['la']:.4f} anchor:{s['anchor']:.4f} lr:{lr:.2e}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _save(self, epoch, step):
|
|
183
|
+
os.makedirs(self.args.save_dir, exist_ok=True)
|
|
184
|
+
ckp = os.path.join(self.args.save_dir, f"distill_{self.config.hidden_size}_moe.pth")
|
|
185
|
+
self.model.eval()
|
|
186
|
+
state = self.model.state_dict()
|
|
187
|
+
torch.save({k: v.half().cpu() for k, v in state.items()}, ckp)
|
|
188
|
+
self.model.train()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_distill_loader(data_path, tokenizer, max_seq_len, batch_size):
|
|
192
|
+
ds = SFTDataset(data_path, tokenizer, max_length=max_seq_len)
|
|
193
|
+
return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=0)
|