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,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,13 @@
1
+ from chronos.serving.vllm_adapter import (
2
+ register_chronos_with_vllm,
3
+ set_available_expert_masks,
4
+ is_available,
5
+ HAS_VLLM,
6
+ )
7
+
8
+ __all__ = [
9
+ "register_chronos_with_vllm",
10
+ "set_available_expert_masks",
11
+ "is_available",
12
+ "HAS_VLLM",
13
+ ]
@@ -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)