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,159 @@
1
+ """
2
+ chronos/mlx/inference.py — MLX inference engine for Project Chronos.
3
+
4
+ Mirrors ChronosInferenceEngine (PyTorch) but uses:
5
+ - ChronosMLXModel for forward passes
6
+ - MLXExpertStore (unified memory, no H2D stream)
7
+ - mx.compile() for static-graph acceleration
8
+ - Async prefetch via Python threading (SSD → unified memory)
9
+
10
+ Usage:
11
+ engine = ChronosMLXInferenceEngine(model, config)
12
+ for token_id in engine.generate("Explain how Chronos moves expert IO into prefill.", max_new_tokens=128):
13
+ print(tokenizer.decode([token_id]), end="", flush=True)
14
+ """
15
+ import threading
16
+ import queue
17
+ from typing import Iterator, List, Optional
18
+
19
+ import mlx.core as mx
20
+ import mlx.nn as nn
21
+
22
+ from chronos.mlx.expert_store import MLXExpertStore
23
+
24
+
25
+ class ChronosMLXInferenceEngine:
26
+ """
27
+ Streaming token-by-token inference with async expert prefetch.
28
+
29
+ On Apple Silicon the prefetch thread reads .npy files from SSD and
30
+ calls mx.array() to bring weights into unified memory. The main
31
+ thread calls mx.eval() (promote_to_vram) before the next forward
32
+ pass — Metal overlaps the IO with prior computation automatically.
33
+ """
34
+
35
+ def __init__(self, model, config, ssd_dir: str = "./expert_cache_mlx"):
36
+ self.model = model
37
+ self.config = config
38
+ self.store = MLXExpertStore(model, config, ssd_dir=ssd_dir)
39
+ self._prefetch_q: queue.Queue = queue.Queue(maxsize=config.prefetch_depth * 2)
40
+ self._stop = threading.Event()
41
+ self._prefetch_thread = threading.Thread(
42
+ target=self._prefetch_loop, daemon=True, name="mlx-prefetch"
43
+ )
44
+ self._prefetch_thread.start()
45
+
46
+ # ── Prefetch background thread ────────────────────────────────
47
+
48
+ def _prefetch_loop(self):
49
+ while not self._stop.is_set():
50
+ try:
51
+ expert_ids: List[int] = self._prefetch_q.get(timeout=0.05)
52
+ except queue.Empty:
53
+ continue
54
+ self.store.prefetch_to_ram(expert_ids)
55
+ self._prefetch_q.task_done()
56
+
57
+ def _schedule_prefetch(self, expert_ids: List[int]):
58
+ try:
59
+ self._prefetch_q.put_nowait(expert_ids)
60
+ except queue.Full:
61
+ pass
62
+
63
+ def stop(self):
64
+ self._stop.set()
65
+
66
+ # ── Generation loop ───────────────────────────────────────────
67
+
68
+ def generate(
69
+ self,
70
+ input_ids: mx.array, # [1, S] (already tokenised)
71
+ max_new_tokens: int = 128,
72
+ temperature: float = 0.85,
73
+ top_p: float = 0.9,
74
+ scheduler=None, # Optional[PrefillScheduler] — lazy import
75
+ ) -> Iterator[int]:
76
+ """
77
+ Yields token IDs one at a time.
78
+
79
+ If scheduler is provided (PrefillScheduler), runs prefill-time batch
80
+ loading before the generation loop (dual-layer mode). Otherwise falls
81
+ back to per-token LookaheadRouter-driven prefetch.
82
+ """
83
+ # ── Prefill phase: front-load expert IO ───────────────────────────
84
+ if scheduler is not None:
85
+ import torch, numpy as np
86
+ ids_np = np.array(input_ids.tolist(), dtype=np.int64)
87
+ ids_pt = torch.from_numpy(ids_np)
88
+ scheduler.prepare(ids_pt, device="cpu")
89
+ scheduler.wait()
90
+
91
+ # Prefill forward
92
+ logits, lookahead_probs, cache = self.model(input_ids)
93
+ mx.eval(logits)
94
+
95
+ # Sample from prefill
96
+ next_token = self._sample(logits[:, -1, :], temperature, top_p)
97
+ activated_ids: List[int] = []
98
+ yield int(next_token.item())
99
+
100
+ for _ in range(max_new_tokens - 1):
101
+ if scheduler is not None:
102
+ # Use PrefillScheduler masks (already loaded at prefill)
103
+ n_layers = len(self.model.layers)
104
+ avail_masks = [self.store.vram_availability_mask()] * n_layers
105
+ else:
106
+ # LookaheadRouter-driven prefetch
107
+ if lookahead_probs is not None:
108
+ future_ids = self._predict_future_experts(lookahead_probs)
109
+ self._schedule_prefetch(future_ids)
110
+ avail_masks = self._build_avail_masks(next_token)
111
+
112
+ token_in = next_token.reshape(1, 1)
113
+ logits, lookahead_probs, cache = self.model(
114
+ token_in,
115
+ cache=cache,
116
+ available_expert_masks=avail_masks,
117
+ )
118
+ mx.eval(logits)
119
+
120
+ next_token = self._sample(logits[:, -1, :], temperature, top_p)
121
+ mx.eval(next_token)
122
+ tok_id = int(next_token.item())
123
+ activated_ids.append(tok_id)
124
+ yield tok_id
125
+
126
+ # Update frequency heuristic after generation
127
+ if scheduler is not None:
128
+ scheduler.record_activation(list(set(activated_ids)))
129
+
130
+ def _predict_future_experts(self, lookahead_probs: mx.array) -> List[int]:
131
+ """Extract top-1 expert IDs for t+1 and t+2 from lookahead probs."""
132
+ # lookahead_probs: [B, S, K+1, E] — take last token, steps 1..K
133
+ future = lookahead_probs[0, -1, 1:, :] # [K, E]
134
+ top_ids = mx.argmax(future, axis=-1).tolist() # [K]
135
+ return list(set(top_ids))
136
+
137
+ def _build_avail_masks(self, _token) -> List[mx.array]:
138
+ """Promote prefetched experts and return per-layer availability masks."""
139
+ n_layers = len(self.model.layers)
140
+ mask = self.store.vram_availability_mask()
141
+ return [mask] * n_layers
142
+
143
+ @staticmethod
144
+ def _sample(logits: mx.array, temperature: float, top_p: float) -> mx.array:
145
+ if temperature == 0:
146
+ return mx.argmax(logits, axis=-1)
147
+ scaled = logits / temperature
148
+ probs = mx.softmax(scaled, axis=-1)
149
+ # top-p nucleus sampling
150
+ sorted_idx = mx.argsort(-probs, axis=-1)
151
+ sorted_probs = mx.take_along_axis(probs, sorted_idx, axis=-1)
152
+ cumsum = mx.cumsum(sorted_probs, axis=-1)
153
+ # mask tokens beyond top-p threshold
154
+ cutoff = mx.array(top_p)
155
+ nucleus = (cumsum - sorted_probs) < cutoff
156
+ filtered = mx.where(nucleus, sorted_probs, mx.zeros_like(sorted_probs))
157
+ filtered = filtered / (filtered.sum(axis=-1, keepdims=True) + 1e-9)
158
+ sample_idx = mx.random.categorical(mx.log(filtered + 1e-9))
159
+ return mx.take(sorted_idx[0], sample_idx)
chronos/mlx/model.py ADDED
@@ -0,0 +1,181 @@
1
+ """
2
+ chronos/mlx/model.py — Full MLX Chronos model.
3
+
4
+ Architecture mirrors the PyTorch version (ChronosForCausalLM) but uses:
5
+ - mlx.nn.Module throughout
6
+ - MLAAttentionMLX (even layers) / SlidingWindowAttentionMLX (odd layers)
7
+ - ChronosMLXMOE for feed-forward
8
+ - LookaheadRouter as a plain Linear projection
9
+ - mx.compile()-friendly: no Python control flow on runtime shapes
10
+
11
+ On Apple Silicon, mx.eval() materialises arrays into Metal shared memory.
12
+ Prefetch = eagerly eval expert weights during the prefetch window.
13
+ """
14
+ import math
15
+ import mlx.core as mx
16
+ import mlx.nn as nn
17
+
18
+ from chronos.mlx.attention import MLAAttentionMLX, SlidingWindowAttentionMLX
19
+ from chronos.mlx.moe import ChronosMLXMOE, FeedForwardMLX
20
+
21
+
22
+ class RMSNormMLX(nn.Module):
23
+ def __init__(self, dim: int, eps: float = 1e-5):
24
+ super().__init__()
25
+ self.weight = mx.ones((dim,))
26
+ self.eps = eps
27
+
28
+ def __call__(self, x: mx.array) -> mx.array:
29
+ return nn.RMSNorm(x.shape[-1], eps=self.eps)(x) * self.weight
30
+
31
+
32
+ class LookaheadRouterMLX(nn.Module):
33
+ """Predicts expert probabilities for t, t+1, ..., t+K steps."""
34
+
35
+ def __init__(self, config):
36
+ super().__init__()
37
+ K = getattr(config, 'lookahead_steps', 2)
38
+ self.lookahead_steps = K
39
+ self.proj = nn.Linear(
40
+ config.hidden_size,
41
+ config.num_experts * (K + 1),
42
+ bias=False,
43
+ )
44
+ self.num_experts = config.num_experts
45
+
46
+ def __call__(self, x: mx.array) -> mx.array:
47
+ B, S, H = x.shape
48
+ out = self.proj(x) # [B, S, E*(K+1)]
49
+ out = out.reshape(B, S, self.lookahead_steps + 1, self.num_experts)
50
+ return mx.softmax(out, axis=-1) # [B, S, K+1, E]
51
+
52
+
53
+ class ChronosMLXBlock(nn.Module):
54
+ def __init__(self, layer_id: int, config):
55
+ super().__init__()
56
+ self.input_norm = RMSNormMLX(config.hidden_size, eps=config.rms_norm_eps)
57
+ self.post_norm = RMSNormMLX(config.hidden_size, eps=config.rms_norm_eps)
58
+
59
+ use_hybrid = getattr(config, 'use_hybrid_attention', True)
60
+ if use_hybrid:
61
+ if layer_id % 2 == 0:
62
+ self.self_attn = MLAAttentionMLX(config)
63
+ else:
64
+ self.self_attn = SlidingWindowAttentionMLX(config)
65
+ else:
66
+ self.self_attn = SlidingWindowAttentionMLX(config)
67
+
68
+ if getattr(config, 'use_moe', True):
69
+ self.mlp = ChronosMLXMOE(config)
70
+ else:
71
+ self.mlp = FeedForwardMLX(config.hidden_size, config.intermediate_size)
72
+
73
+ def __call__(
74
+ self,
75
+ x: mx.array,
76
+ mask: mx.array = None,
77
+ cache=None,
78
+ available_expert_mask: mx.array = None,
79
+ ):
80
+ h, new_cache = self.self_attn(self.input_norm(x), mask=mask, cache=cache)
81
+ x = x + h
82
+ mlp_in = self.post_norm(x)
83
+ if isinstance(self.mlp, ChronosMLXMOE):
84
+ x = x + self.mlp(mlp_in, available_expert_mask=available_expert_mask)
85
+ else:
86
+ x = x + self.mlp(mlp_in)
87
+ return x, new_cache
88
+
89
+
90
+ class ChronosMLXModel(nn.Module):
91
+ """
92
+ Full Chronos model in MLX.
93
+
94
+ forward() returns (logits [B, S, V], lookahead_probs [B, S, K+1, E])
95
+ Cache: list of per-layer attention caches (passed back in, None at start).
96
+ """
97
+
98
+ def __init__(self, config):
99
+ super().__init__()
100
+ self.config = config
101
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
102
+ self.layers = [ChronosMLXBlock(i, config)
103
+ for i in range(config.num_hidden_layers)]
104
+ self.norm = RMSNormMLX(config.hidden_size, eps=config.rms_norm_eps)
105
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
106
+
107
+ # LookaheadRouter inserted after block 0
108
+ self.lookahead_router = LookaheadRouterMLX(config)
109
+
110
+ def __call__(
111
+ self,
112
+ input_ids: mx.array, # [B, S]
113
+ cache=None, # list of per-layer caches, or None
114
+ available_expert_masks=None, # list of [E] float masks, or None
115
+ ):
116
+ B, S = input_ids.shape
117
+ x = self.embed_tokens(input_ids) # [B, S, H]
118
+
119
+ # Causal mask — only needed for prefill (S > 1); decode (S==1) needs no mask
120
+ mask = None
121
+ if S > 1:
122
+ mask = nn.MultiHeadAttention.create_additive_causal_mask(S)
123
+ mask = mask.astype(x.dtype)
124
+
125
+ new_caches = []
126
+ lookahead_probs = None
127
+
128
+ if cache is None:
129
+ cache = [None] * len(self.layers)
130
+
131
+ for i, layer in enumerate(self.layers):
132
+ avail = (available_expert_masks[i]
133
+ if available_expert_masks is not None else None)
134
+ x, lyr_cache = layer(x, mask=mask, cache=cache[i],
135
+ available_expert_mask=avail)
136
+ new_caches.append(lyr_cache)
137
+
138
+ # Lookahead routing after block 0
139
+ if i == 0:
140
+ lookahead_probs = self.lookahead_router(x)
141
+
142
+ x = self.norm(x)
143
+ logits = self.lm_head(x)
144
+ return logits, lookahead_probs, new_caches
145
+
146
+ @staticmethod
147
+ def from_chronos_pytorch(pt_model, config):
148
+ """
149
+ Convert a trained PyTorch ChronosForCausalLM to ChronosMLXModel.
150
+ Copies weights layer by layer via numpy as bridge.
151
+ """
152
+ import numpy as np
153
+ import torch
154
+
155
+ mlx_model = ChronosMLXModel(config)
156
+
157
+ def _to_mx(pt_tensor):
158
+ return mx.array(pt_tensor.detach().float().cpu().numpy())
159
+
160
+ # Embedding + head
161
+ sd = pt_model.state_dict()
162
+ mlx_model.embed_tokens.weight = _to_mx(sd["model.embed_tokens.weight"])
163
+ mlx_model.lm_head.weight = _to_mx(sd["lm_head.weight"])
164
+ mlx_model.norm.weight = _to_mx(sd["model.norm.weight"])
165
+
166
+ for i in range(config.num_hidden_layers):
167
+ prefix = f"model.layers.{i}"
168
+ blk = mlx_model.layers[i]
169
+ blk.input_norm.weight = _to_mx(sd[f"{prefix}.input_layernorm.weight"])
170
+ blk.post_norm.weight = _to_mx(sd[f"{prefix}.post_feedforward_layernorm.weight"])
171
+ # Attention weights are mapped by name if they match
172
+ attn = blk.self_attn
173
+ for attr in vars(attn):
174
+ pt_key = f"{prefix}.self_attn.{attr}.weight"
175
+ if pt_key in sd and hasattr(getattr(attn, attr, None), '__class__'):
176
+ try:
177
+ setattr(attn, attr,
178
+ type(getattr(attn, attr))(_to_mx(sd[pt_key])))
179
+ except Exception:
180
+ pass
181
+ return mlx_model
chronos/mlx/moe.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ chronos/mlx/moe.py — MLX-native ChronosMOEFeedForward.
3
+
4
+ Soft gating is expressed as weighted sum (compile-safe):
5
+ out = avail[i] * expert_out + (1 - avail[i]) * shared_out
6
+
7
+ mx.compile() traces a single graph — no Python branches on mask values.
8
+ """
9
+ import mlx.core as mx
10
+ import mlx.nn as nn
11
+
12
+
13
+ class FeedForwardMLX(nn.Module):
14
+ """SwiGLU FFN matching minimind's FeedForward structure."""
15
+
16
+ def __init__(self, hidden_size: int, intermediate_size: int):
17
+ super().__init__()
18
+ self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
19
+ self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
20
+ self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
21
+
22
+ def __call__(self, x: mx.array) -> mx.array:
23
+ return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
24
+
25
+
26
+ class ChronosMLXMOE(nn.Module):
27
+ """
28
+ MLX-native Mixture-of-Experts with:
29
+ - LookaheadRouter output stored as last_router_probs
30
+ - Compile-safe soft gating (avail mask → float multiply)
31
+ - Shared expert fallback at zero I/O cost (unified memory)
32
+ """
33
+
34
+ def __init__(self, config):
35
+ super().__init__()
36
+ self.num_experts = config.num_experts
37
+ self.num_experts_per_tok = config.num_experts_per_tok
38
+ self.norm_topk_prob = config.norm_topk_prob
39
+ H = config.hidden_size
40
+ I = config.moe_intermediate_size
41
+
42
+ self.gate = nn.Linear(H, config.num_experts, bias=False)
43
+ self.experts = [FeedForwardMLX(H, I) for _ in range(config.num_experts)]
44
+ self.shared_experts = [
45
+ FeedForwardMLX(H, I) for _ in range(config.num_shared_experts)
46
+ ]
47
+ self.last_router_probs = None # [B, S, E] stored after each forward
48
+
49
+ def _shared_out(self, x: mx.array) -> mx.array:
50
+ return sum(e(x) for e in self.shared_experts) / len(self.shared_experts)
51
+
52
+ def __call__(
53
+ self,
54
+ x: mx.array,
55
+ available_expert_mask: mx.array = None,
56
+ ) -> mx.array:
57
+ """
58
+ x: [B, S, H]
59
+ available_expert_mask: [num_experts] float (1.0 = in GPU, 0.0 = miss)
60
+ or None for training (all available).
61
+ """
62
+ B, S, H = x.shape
63
+ x_flat = x.reshape(-1, H) # [N, H]
64
+
65
+ scores = mx.softmax(self.gate(x_flat), axis=-1) # [N, E]
66
+ self.last_router_probs = scores.reshape(B, S, self.num_experts)
67
+
68
+ # top-k routing
69
+ topk_idx = mx.argpartition(-scores, kth=self.num_experts_per_tok - 1, axis=-1)
70
+ topk_idx = topk_idx[:, :self.num_experts_per_tok] # [N, K]
71
+ topk_weight = mx.take_along_axis(scores, topk_idx, axis=-1) # [N, K]
72
+ if self.norm_topk_prob:
73
+ topk_weight = topk_weight / (topk_weight.sum(axis=-1, keepdims=True) + 1e-20)
74
+
75
+ y = mx.zeros_like(x_flat)
76
+
77
+ if available_expert_mask is None:
78
+ # Training: standard MoE scatter
79
+ for i, expert in enumerate(self.experts):
80
+ # mask over tokens where expert i is selected
81
+ sel = (topk_idx == i) # [N, K] bool
82
+ tok_mask = sel.any(axis=-1) # [N] bool
83
+ w = mx.where(sel, topk_weight, 0.0).sum(axis=-1, keepdims=True) # [N, 1]
84
+ contrib = expert(x_flat) * w * tok_mask[:, None]
85
+ y = y + contrib
86
+ else:
87
+ # Inference: compile-safe soft gating
88
+ avail = available_expert_mask.astype(x_flat.dtype) # [E] float
89
+ shared_out = self._shared_out(x_flat) # [N, H]
90
+
91
+ for i, expert in enumerate(self.experts):
92
+ sel = (topk_idx == i)
93
+ tok_mask = sel.any(axis=-1)
94
+ w = mx.where(sel, topk_weight, 0.0).sum(axis=-1, keepdims=True)
95
+
96
+ expert_out = expert(x_flat)
97
+ # avail[i]==1 → expert; avail[i]==0 → shared fallback
98
+ blended = avail[i] * expert_out + (1.0 - avail[i]) * shared_out
99
+ contrib = blended * w * tok_mask[:, None]
100
+ y = y + contrib
101
+
102
+ return y.reshape(B, S, H)
@@ -0,0 +1,6 @@
1
+ from .config import ChronosConfig
2
+ from .lookahead_router import LookaheadRouter
3
+ from .moe_chronos import ChronosMOEFeedForward
4
+ from .model_chronos import ChronosForCausalLM
5
+ from .temporal_loss import temporal_locality_loss, total_loss
6
+ from .hybrid_attention import MLAAttention, SlidingWindowAttention, make_attention
@@ -0,0 +1,54 @@
1
+ import sys
2
+ import chronos.deps # ensure minimind on sys.path
3
+
4
+ from model.model_minimind import MiniMindConfig
5
+
6
+
7
+ class ChronosConfig(MiniMindConfig):
8
+ """
9
+ Extends MiniMindConfig with Project Chronos-specific fields:
10
+ - lookahead_steps: how many future token steps to predict expert routing for
11
+ - num_shared_experts: experts always resident in VRAM as fallback
12
+ - lambda_balance / lambda_temporal: loss coefficients
13
+ - vram_budget_gb: VRAM cap for expert cache
14
+ - prefetch_depth: async prefetch queue depth
15
+ """
16
+ model_type = "chronos"
17
+
18
+ def __init__(self, **kwargs):
19
+ self.lookahead_steps: int = kwargs.pop("lookahead_steps", 2)
20
+ self.num_shared_experts: int = kwargs.pop("num_shared_experts", 1)
21
+ self.lambda_balance: float = kwargs.pop("lambda_balance", 5e-4)
22
+ self.lambda_temporal: float = kwargs.pop("lambda_temporal", 1e-3)
23
+ self.vram_budget_gb: float = kwargs.pop("vram_budget_gb", 4.0)
24
+ self.prefetch_depth: int = kwargs.pop("prefetch_depth", 2)
25
+ # Hybrid attention params
26
+ self.use_hybrid_attention: bool = kwargs.pop("use_hybrid_attention", True)
27
+ self.kv_latent_dim: int = kwargs.pop("kv_latent_dim", 64) # MLA compression dim
28
+ self.rope_dim: int = kwargs.pop("rope_dim", 32) # RoPE portion of head_dim
29
+ self.sliding_window_size: int = kwargs.pop("sliding_window_size", 2048)
30
+ # Pinned memory safety: max RAM for expert buffers (fraction of physical RAM)
31
+ self.pinned_memory_max_fraction: float = kwargs.pop("pinned_memory_max_fraction", 0.25)
32
+ # Storage backend for expert weights on SSD.
33
+ # "safetensors" packs each Louvain cluster into one .ctsr file +
34
+ # cluster_manifest.json (mmap-friendly, sequential reads).
35
+ # "pt" keeps the legacy one-pickle-per-expert layout.
36
+ self.storage_format: str = kwargs.pop("storage_format", "safetensors")
37
+ # Optional path to a precomputed cluster_manifest.json. When set,
38
+ # CacheManager will attach this layout instead of the one in ssd_dir.
39
+ self.cluster_manifest_path = kwargs.pop("cluster_manifest_path", None)
40
+ # M2: weight on the lookahead supervision loss
41
+ self.lambda_lookahead: float = kwargs.pop("lambda_lookahead", 0.1)
42
+ # M4: weight on the router KL anchor (alignment stages only).
43
+ # Prevents SFT/DPO/ORPO/GRPO gradients from drifting routing away
44
+ # from the pretrained distribution that the cluster layout was
45
+ # optimized for. Typical values:
46
+ # pretrain: 0.0, SFT: 0.01, DPO/ORPO/GRPO: 0.1
47
+ self.lambda_router_anchor: float = kwargs.pop("lambda_router_anchor", 0.0)
48
+ # M7: regularization defaults. MiniMindConfig defaults dropout=0.0
49
+ # and weight_decay isn't even a config field — both major causes
50
+ # of overfitting on small corpora. Override here.
51
+ kwargs.setdefault("dropout", 0.1)
52
+ # Force MoE on
53
+ kwargs.setdefault("use_moe", True)
54
+ super().__init__(**kwargs)
chronos/model/hf_io.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ chronos/model/hf_io.py
3
+
4
+ HuggingFace save/load helpers for ChronosForCausalLM.
5
+
6
+ ``ChronosForCausalLM`` already inherits ``transformers.PreTrainedModel`` so
7
+ ``.from_pretrained()`` / ``.save_pretrained()`` technically work. The pieces
8
+ this module adds:
9
+
10
+ 1. Register ``ChronosConfig`` with ``AutoConfig`` and ``ChronosForCausalLM``
11
+ with ``AutoModelForCausalLM`` so users can do
12
+ ``AutoModelForCausalLM.from_pretrained("path/to/chronos")`` without
13
+ ``trust_remote_code=True``.
14
+
15
+ 2. ``save_chronos_pretrained`` / ``load_chronos_pretrained`` helpers that
16
+ also preserve the Chronos expert-cache layout (``cluster_manifest.json``
17
+ alongside the ``.ctsr`` cluster files) so a round-trip keeps the
18
+ offloaded SSD setup intact.
19
+
20
+ Importing this module is side-effect-ful: registration happens at import
21
+ time. ``chronos/__init__.py`` imports it so ordinary usage just works.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import shutil
27
+ from typing import Optional
28
+
29
+ import torch
30
+
31
+
32
+ def _register_auto_classes():
33
+ """Register Chronos classes with Transformers AutoXXX factories."""
34
+ try:
35
+ from transformers import AutoConfig, AutoModelForCausalLM
36
+ except Exception:
37
+ return
38
+ from chronos.model.config import ChronosConfig
39
+ from chronos.model.model_chronos import ChronosForCausalLM
40
+
41
+ # Idempotent: Transformers raises if we try to register the same config
42
+ # twice. We catch and ignore.
43
+ try:
44
+ AutoConfig.register("chronos", ChronosConfig)
45
+ except (ValueError, KeyError):
46
+ pass
47
+ try:
48
+ AutoModelForCausalLM.register(ChronosConfig, ChronosForCausalLM)
49
+ except (ValueError, KeyError):
50
+ pass
51
+
52
+
53
+ _register_auto_classes()
54
+
55
+
56
+ def save_chronos_pretrained(
57
+ model,
58
+ save_directory: str,
59
+ expert_cache_dir: Optional[str] = None,
60
+ safe_serialization: bool = True,
61
+ ) -> None:
62
+ """Save a Chronos model in HF format (``model.safetensors`` + ``config.json``),
63
+ and copy the cluster manifest + .ctsr files into ``save_directory/expert_cache/``
64
+ if ``expert_cache_dir`` is provided so the offloaded layout travels with
65
+ the checkpoint."""
66
+ os.makedirs(save_directory, exist_ok=True)
67
+
68
+ # MiniMind/Chronos tie ``lm_head.weight`` to ``model.embed_tokens.weight``
69
+ # but doesn't declare it via ``_tied_weights_keys``, which trips
70
+ # safetensors' shared-tensor detector. Clone lm_head once before saving;
71
+ # restore the tie after.
72
+ lm_head = getattr(model, "lm_head", None)
73
+ embed = getattr(getattr(model, "model", model), "embed_tokens", None)
74
+ re_tie = (
75
+ lm_head is not None and embed is not None
76
+ and lm_head.weight.data_ptr() == embed.weight.data_ptr()
77
+ )
78
+ if re_tie:
79
+ original = lm_head.weight
80
+ lm_head.weight = torch.nn.Parameter(original.detach().clone())
81
+
82
+ try:
83
+ model.save_pretrained(save_directory, safe_serialization=safe_serialization)
84
+ finally:
85
+ if re_tie:
86
+ lm_head.weight = embed.weight # restore the tie
87
+
88
+ if expert_cache_dir and os.path.isdir(expert_cache_dir):
89
+ dst = os.path.join(save_directory, "expert_cache")
90
+ os.makedirs(dst, exist_ok=True)
91
+ for name in os.listdir(expert_cache_dir):
92
+ if name.endswith((".ctsr", ".json")):
93
+ shutil.copy2(
94
+ os.path.join(expert_cache_dir, name),
95
+ os.path.join(dst, name),
96
+ )
97
+
98
+
99
+ def load_chronos_pretrained(
100
+ save_directory: str,
101
+ device: Optional[str] = None,
102
+ **from_pretrained_kwargs,
103
+ ):
104
+ """Load a Chronos model saved via ``save_chronos_pretrained``.
105
+
106
+ If ``save_directory/expert_cache/cluster_manifest.json`` is present,
107
+ the path is returned alongside the model so callers can wire it into
108
+ ``CacheManager(ssd_dir=...)`` or ``ExpertStore.attach_cluster_manifest(...)``.
109
+
110
+ Returns ``(model, expert_cache_dir_or_None)``.
111
+ """
112
+ from chronos.model.model_chronos import ChronosForCausalLM
113
+ model = ChronosForCausalLM.from_pretrained(save_directory, **from_pretrained_kwargs)
114
+
115
+ # MiniMind/Chronos precompute RoPE frequencies as *persistent* buffers.
116
+ # When the checkpoint is saved as fp16 and reloaded, these deterministic
117
+ # buffers get truncated to zeros/NaNs. Recompute them from scratch —
118
+ # they are a pure function of config.
119
+ _rehydrate_rope_buffers(model)
120
+
121
+ if device is not None:
122
+ model = model.to(device)
123
+
124
+ cache_dir = os.path.join(save_directory, "expert_cache")
125
+ if os.path.exists(os.path.join(cache_dir, "cluster_manifest.json")):
126
+ return model, cache_dir
127
+ return model, None
128
+
129
+
130
+ def _rehydrate_rope_buffers(model) -> None:
131
+ """Recompute ``freqs_cos`` and ``freqs_sin`` on the inner ChronosModel.
132
+ These are deterministic from config; the precompute logic lives inside
133
+ ``ChronosModel.__init__``. We invoke it via a fresh ChronosModel and
134
+ copy the buffers across. Cheap — only runs once per load."""
135
+ try:
136
+ inner = getattr(model, "model", None)
137
+ if inner is None:
138
+ return
139
+ cfg = model.config
140
+ from chronos.model.model_chronos import ChronosModel
141
+ fresh = ChronosModel(cfg)
142
+ if hasattr(inner, "freqs_cos") and hasattr(fresh, "freqs_cos"):
143
+ inner.freqs_cos = fresh.freqs_cos
144
+ if hasattr(inner, "freqs_sin") and hasattr(fresh, "freqs_sin"):
145
+ inner.freqs_sin = fresh.freqs_sin
146
+ except Exception:
147
+ # Rehydration is best-effort; fall through silently so an unexpected
148
+ # config shape doesn't block the load.
149
+ pass