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,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/io/cluster_layout.py
|
|
3
|
+
|
|
4
|
+
Offline expert co-occurrence analysis and clustered storage layout generator.
|
|
5
|
+
|
|
6
|
+
Pipeline:
|
|
7
|
+
1. Collect expert activation logs during a calibration forward pass
|
|
8
|
+
2. Build co-occurrence frequency matrix from logs
|
|
9
|
+
3. Run Louvain community detection (or greedy clustering) to group
|
|
10
|
+
frequently co-activated experts into contiguous physical blocks
|
|
11
|
+
4. Repack model weights on SSD so co-occurring experts are stored
|
|
12
|
+
sequentially → maximizes NVMe sequential read bandwidth
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python -m chronos.io.cluster_layout \
|
|
16
|
+
--model_path ./out/chronos_512_moe.pth \
|
|
17
|
+
--data_path ./dataset/calib.jsonl \
|
|
18
|
+
--output_dir ./expert_cache_clustered
|
|
19
|
+
"""
|
|
20
|
+
import sys
|
|
21
|
+
import chronos.deps # ensure minimind on sys.path
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import json
|
|
25
|
+
import argparse
|
|
26
|
+
import collections
|
|
27
|
+
from typing import List, Dict, Tuple
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import torch
|
|
31
|
+
from torch.utils.data import DataLoader
|
|
32
|
+
from transformers import AutoTokenizer
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Co-occurrence matrix ──────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def collect_activation_log(model, dataloader, device, max_batches=50) -> List[List[int]]:
|
|
38
|
+
"""
|
|
39
|
+
Run calibration forward passes and record which expert is selected
|
|
40
|
+
at each token position (layer 0 routing, top-1).
|
|
41
|
+
|
|
42
|
+
Returns: list of sequences, each sequence is a list of expert IDs.
|
|
43
|
+
"""
|
|
44
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
45
|
+
|
|
46
|
+
moe_layers = [l for l in model.model.layers if isinstance(l.mlp, ChronosMOEFeedForward)]
|
|
47
|
+
if not moe_layers:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
model.eval()
|
|
51
|
+
activation_log = []
|
|
52
|
+
|
|
53
|
+
with torch.no_grad():
|
|
54
|
+
for i, (input_ids, _) in enumerate(dataloader):
|
|
55
|
+
if i >= max_batches:
|
|
56
|
+
break
|
|
57
|
+
input_ids = input_ids.to(device)
|
|
58
|
+
model(input_ids, use_cache=False)
|
|
59
|
+
# Use layer 0 routing as representative
|
|
60
|
+
probs = moe_layers[0].last_router_probs # [B, S, E]
|
|
61
|
+
if probs is None:
|
|
62
|
+
continue
|
|
63
|
+
top1 = probs.argmax(dim=-1) # [B, S]
|
|
64
|
+
for b in range(top1.shape[0]):
|
|
65
|
+
activation_log.append(top1[b].cpu().tolist())
|
|
66
|
+
|
|
67
|
+
return activation_log
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_cooccurrence_matrix(activation_log: List[List[int]], num_experts: int) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Build symmetric co-occurrence matrix C where C[i,j] = number of times
|
|
73
|
+
expert i and expert j are both activated within a sliding window of 4 tokens.
|
|
74
|
+
"""
|
|
75
|
+
C = np.zeros((num_experts, num_experts), dtype=np.float32)
|
|
76
|
+
window = 4
|
|
77
|
+
|
|
78
|
+
for seq in activation_log:
|
|
79
|
+
for t in range(len(seq)):
|
|
80
|
+
anchor = seq[t]
|
|
81
|
+
for k in range(1, window + 1):
|
|
82
|
+
if t + k < len(seq):
|
|
83
|
+
neighbor = seq[t + k]
|
|
84
|
+
if anchor != neighbor:
|
|
85
|
+
C[anchor, neighbor] += 1
|
|
86
|
+
C[neighbor, anchor] += 1
|
|
87
|
+
|
|
88
|
+
# Normalize
|
|
89
|
+
row_sum = C.sum(axis=1, keepdims=True)
|
|
90
|
+
row_sum[row_sum == 0] = 1
|
|
91
|
+
return C / row_sum
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cluster_experts_greedy(
|
|
95
|
+
cooccurrence: np.ndarray,
|
|
96
|
+
n_clusters: int = None,
|
|
97
|
+
) -> List[List[int]]:
|
|
98
|
+
"""
|
|
99
|
+
Greedy co-occurrence clustering: iteratively merge the pair of experts
|
|
100
|
+
with highest co-occurrence score into the same cluster.
|
|
101
|
+
|
|
102
|
+
Falls back to equal-size partitioning if cooccurrence is uniform.
|
|
103
|
+
Returns list of clusters, each cluster is a list of expert IDs.
|
|
104
|
+
"""
|
|
105
|
+
num_experts = cooccurrence.shape[0]
|
|
106
|
+
if n_clusters is None:
|
|
107
|
+
n_clusters = max(2, num_experts // 2)
|
|
108
|
+
|
|
109
|
+
# Start: each expert in its own cluster
|
|
110
|
+
clusters = {i: [i] for i in range(num_experts)}
|
|
111
|
+
expert_to_cluster = {i: i for i in range(num_experts)}
|
|
112
|
+
C = cooccurrence.copy()
|
|
113
|
+
np.fill_diagonal(C, 0)
|
|
114
|
+
|
|
115
|
+
merges_needed = num_experts - n_clusters
|
|
116
|
+
for _ in range(merges_needed):
|
|
117
|
+
# Find highest co-occurrence pair in different clusters
|
|
118
|
+
best_score = -1
|
|
119
|
+
best_i, best_j = 0, 1
|
|
120
|
+
for i in range(num_experts):
|
|
121
|
+
for j in range(i + 1, num_experts):
|
|
122
|
+
ci, cj = expert_to_cluster[i], expert_to_cluster[j]
|
|
123
|
+
if ci != cj and C[i, j] > best_score:
|
|
124
|
+
best_score = C[i, j]
|
|
125
|
+
best_i, best_j = i, j
|
|
126
|
+
|
|
127
|
+
ci = expert_to_cluster[best_i]
|
|
128
|
+
cj = expert_to_cluster[best_j]
|
|
129
|
+
if ci == cj:
|
|
130
|
+
continue
|
|
131
|
+
# Merge cj into ci
|
|
132
|
+
for eid in clusters[cj]:
|
|
133
|
+
expert_to_cluster[eid] = ci
|
|
134
|
+
clusters[ci].extend(clusters.pop(cj))
|
|
135
|
+
|
|
136
|
+
return list(clusters.values())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def try_louvain_clustering(cooccurrence: np.ndarray) -> List[List[int]]:
|
|
140
|
+
"""
|
|
141
|
+
Attempt Louvain community detection via python-louvain.
|
|
142
|
+
Falls back to greedy clustering if not available.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
import community as community_louvain
|
|
146
|
+
import networkx as nx
|
|
147
|
+
|
|
148
|
+
G = nx.Graph()
|
|
149
|
+
n = cooccurrence.shape[0]
|
|
150
|
+
G.add_nodes_from(range(n))
|
|
151
|
+
for i in range(n):
|
|
152
|
+
for j in range(i + 1, n):
|
|
153
|
+
w = float(cooccurrence[i, j])
|
|
154
|
+
if w > 0:
|
|
155
|
+
G.add_edge(i, j, weight=w)
|
|
156
|
+
|
|
157
|
+
partition = community_louvain.best_partition(G, weight='weight')
|
|
158
|
+
clusters_dict = collections.defaultdict(list)
|
|
159
|
+
for node, comm in partition.items():
|
|
160
|
+
clusters_dict[comm].append(node)
|
|
161
|
+
return list(clusters_dict.values())
|
|
162
|
+
|
|
163
|
+
except ImportError:
|
|
164
|
+
return cluster_experts_greedy(cooccurrence)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Storage layout ────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def build_cluster_layout(clusters: List[List[int]]) -> Dict:
|
|
170
|
+
"""
|
|
171
|
+
Build a layout descriptor mapping expert_id → (cluster_id, position_in_cluster).
|
|
172
|
+
"""
|
|
173
|
+
layout = {}
|
|
174
|
+
for cid, cluster in enumerate(clusters):
|
|
175
|
+
for pos, eid in enumerate(cluster):
|
|
176
|
+
layout[eid] = {"cluster_id": cid, "position": pos, "cluster": cluster}
|
|
177
|
+
return layout
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def repack_expert_weights(
|
|
181
|
+
ssd_dir: str,
|
|
182
|
+
clusters: List[List[int]],
|
|
183
|
+
output_dir: str,
|
|
184
|
+
num_layers: int,
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Rewrite expert .pt files in cluster order so that co-occurring experts
|
|
188
|
+
are stored in contiguous blocks on disk (maximizes sequential read).
|
|
189
|
+
|
|
190
|
+
Output naming: expert_l{layer}_c{cluster_id}_p{position}.pt
|
|
191
|
+
Also writes cluster_layout.json for runtime use.
|
|
192
|
+
"""
|
|
193
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
194
|
+
layout_meta = []
|
|
195
|
+
|
|
196
|
+
for cid, cluster in enumerate(clusters):
|
|
197
|
+
for pos, eid in enumerate(cluster):
|
|
198
|
+
for li in range(num_layers):
|
|
199
|
+
src = os.path.join(ssd_dir, f"expert_l{li}_e{eid}.pt")
|
|
200
|
+
dst = os.path.join(output_dir, f"expert_l{li}_c{cid}_p{pos}.pt")
|
|
201
|
+
if os.path.exists(src) and not os.path.exists(dst):
|
|
202
|
+
import shutil
|
|
203
|
+
shutil.copy2(src, dst)
|
|
204
|
+
layout_meta.append({
|
|
205
|
+
"expert_id": eid,
|
|
206
|
+
"cluster_id": cid,
|
|
207
|
+
"position": pos,
|
|
208
|
+
"cluster_members": cluster,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
layout_path = os.path.join(output_dir, "cluster_layout.json")
|
|
212
|
+
with open(layout_path, "w") as f:
|
|
213
|
+
json.dump(layout_meta, f, indent=2)
|
|
214
|
+
|
|
215
|
+
print(f"Cluster layout saved: {layout_path}")
|
|
216
|
+
print(f"Clusters ({len(clusters)}):")
|
|
217
|
+
for cid, cluster in enumerate(clusters):
|
|
218
|
+
print(f" Cluster {cid}: experts {cluster}")
|
|
219
|
+
return layout_path
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_cluster_layout(layout_path: str) -> Dict[int, Dict]:
|
|
223
|
+
"""Load cluster_layout.json → {expert_id: metadata}."""
|
|
224
|
+
with open(layout_path) as f:
|
|
225
|
+
meta = json.load(f)
|
|
226
|
+
return {entry["expert_id"]: entry for entry in meta}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── Safetensors cluster repack ────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
def repack_expert_weights_safetensors(
|
|
232
|
+
model,
|
|
233
|
+
clusters: List[List[int]],
|
|
234
|
+
output_dir: str,
|
|
235
|
+
num_layers: int = None,
|
|
236
|
+
dtype: "torch.dtype" = None,
|
|
237
|
+
) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Pack every expert of a live `ChronosForCausalLM` into cluster .ctsr files
|
|
240
|
+
plus a cluster_manifest.json. This is the runtime path consumed by
|
|
241
|
+
ExpertStore when `storage_format == "safetensors"`.
|
|
242
|
+
|
|
243
|
+
Unlike the legacy `repack_expert_weights` (which copies .pt files around),
|
|
244
|
+
this reads weights directly from the model's MoE layers so it works even
|
|
245
|
+
before any offload has happened.
|
|
246
|
+
|
|
247
|
+
Returns the path to cluster_manifest.json.
|
|
248
|
+
"""
|
|
249
|
+
import torch as _torch
|
|
250
|
+
from chronos.io.storage import ClusterStorage
|
|
251
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
252
|
+
|
|
253
|
+
moe_layers = [
|
|
254
|
+
layer.mlp for layer in model.model.layers
|
|
255
|
+
if isinstance(layer.mlp, ChronosMOEFeedForward)
|
|
256
|
+
]
|
|
257
|
+
if num_layers is None:
|
|
258
|
+
num_layers = len(moe_layers)
|
|
259
|
+
num_experts = moe_layers[0].num_experts if moe_layers else 0
|
|
260
|
+
|
|
261
|
+
manifest_path = ClusterStorage.write_clusters(
|
|
262
|
+
moe_layers=moe_layers,
|
|
263
|
+
clusters=clusters,
|
|
264
|
+
output_dir=output_dir,
|
|
265
|
+
num_layers=num_layers,
|
|
266
|
+
num_experts=num_experts,
|
|
267
|
+
dtype=dtype if dtype is not None else _torch.float16,
|
|
268
|
+
)
|
|
269
|
+
return manifest_path
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── CLI entry point ───────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def main():
|
|
275
|
+
parser = argparse.ArgumentParser(description="Chronos Expert Cluster Layout Generator")
|
|
276
|
+
parser.add_argument("--model_path", type=str, required=True)
|
|
277
|
+
parser.add_argument("--data_path", type=str, required=True)
|
|
278
|
+
parser.add_argument("--ssd_dir", type=str, default="./expert_cache")
|
|
279
|
+
parser.add_argument("--output_dir", type=str, default="./expert_cache_clustered")
|
|
280
|
+
parser.add_argument("--device", type=str, default="cpu")
|
|
281
|
+
parser.add_argument("--max_batches", type=int, default=50)
|
|
282
|
+
parser.add_argument("--batch_size", type=int, default=4)
|
|
283
|
+
parser.add_argument("--max_seq_len", type=int, default=256)
|
|
284
|
+
args = parser.parse_args()
|
|
285
|
+
|
|
286
|
+
from chronos.model.config import ChronosConfig
|
|
287
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
288
|
+
from dataset.lm_dataset import PretrainDataset
|
|
289
|
+
|
|
290
|
+
config = ChronosConfig(use_moe=True)
|
|
291
|
+
model = ChronosForCausalLM(config).to(args.device)
|
|
292
|
+
weights = torch.load(args.model_path, map_location=args.device)
|
|
293
|
+
model.load_state_dict(weights, strict=False)
|
|
294
|
+
|
|
295
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
296
|
+
chronos.deps.get_tokenizer_path()
|
|
297
|
+
)
|
|
298
|
+
dataset = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
|
299
|
+
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False)
|
|
300
|
+
|
|
301
|
+
print("Collecting activation log...")
|
|
302
|
+
log = collect_activation_log(model, loader, args.device, args.max_batches)
|
|
303
|
+
print(f" {len(log)} sequences collected")
|
|
304
|
+
|
|
305
|
+
C = build_cooccurrence_matrix(log, config.num_experts)
|
|
306
|
+
print(f"Co-occurrence matrix:\n{np.round(C, 3)}")
|
|
307
|
+
|
|
308
|
+
clusters = try_louvain_clustering(C)
|
|
309
|
+
layout_path = repack_expert_weights(
|
|
310
|
+
args.ssd_dir, clusters, args.output_dir,
|
|
311
|
+
num_layers=config.num_hidden_layers,
|
|
312
|
+
)
|
|
313
|
+
print(f"Done. Layout: {layout_path}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
main()
|