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,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()