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
chronos/mlx/inference.py
ADDED
|
@@ -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
|
chronos/model/config.py
ADDED
|
@@ -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
|