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,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/data/flexible_dataset.py
|
|
3
|
+
|
|
4
|
+
Streaming JSONL loaders with byte-offset indexing. Three variants cover
|
|
5
|
+
the six Chronos training stages without ever loading the file into RAM:
|
|
6
|
+
|
|
7
|
+
FlexibleDataset — pretrain / language modeling. Auto-detects
|
|
8
|
+
common text-field schemas (text, content,
|
|
9
|
+
instruction+output, conversations, messages).
|
|
10
|
+
StreamingSFTDataset — supervised fine-tuning. Uses the tokenizer's
|
|
11
|
+
chat template; only assistant tokens contribute
|
|
12
|
+
to the loss (label = -100 elsewhere). Drop-in
|
|
13
|
+
for minimind's SFTDataset.
|
|
14
|
+
StreamingDPODataset — preference pairs. Returns the dict shape DPO /
|
|
15
|
+
ORPO trainers expect.
|
|
16
|
+
|
|
17
|
+
Each dataset stores only `offsets: list[int]` (8 bytes/sample) plus the
|
|
18
|
+
file path. `__getitem__` does seek+readline+json.loads on demand, so a
|
|
19
|
+
multi-GB corpus uses < 1 GB of resident memory.
|
|
20
|
+
|
|
21
|
+
The file handle is opened lazily per process (re-opens after fork so
|
|
22
|
+
DataLoader num_workers > 0 still works) and protected by a thread lock
|
|
23
|
+
so num_workers = 0 paths with concurrent access are safe.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import threading
|
|
30
|
+
|
|
31
|
+
import torch
|
|
32
|
+
from torch.utils.data import Dataset
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Field-name auto-detection (FlexibleDataset only) ──────────────────
|
|
36
|
+
|
|
37
|
+
_TEXT_KEYS = ("text", "content", "story", "passage", "document", "article")
|
|
38
|
+
_PAIR_KEYS = (
|
|
39
|
+
("instruction", "output"),
|
|
40
|
+
("instruction", "response"),
|
|
41
|
+
("prompt", "response"),
|
|
42
|
+
("prompt", "completion"),
|
|
43
|
+
("input", "output"),
|
|
44
|
+
("question", "answer"),
|
|
45
|
+
("query", "answer"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_text(record: dict) -> str:
|
|
50
|
+
for k in _TEXT_KEYS:
|
|
51
|
+
if k in record and record[k]:
|
|
52
|
+
return str(record[k])
|
|
53
|
+
for k1, k2 in _PAIR_KEYS:
|
|
54
|
+
if k1 in record and k2 in record:
|
|
55
|
+
parts = [str(record[k1]).strip(), str(record[k2]).strip()]
|
|
56
|
+
return "\n".join(p for p in parts if p)
|
|
57
|
+
if "conversations" in record and isinstance(record["conversations"], list):
|
|
58
|
+
return " ".join(
|
|
59
|
+
str(c.get("value", c.get("content", "")))
|
|
60
|
+
for c in record["conversations"]
|
|
61
|
+
)
|
|
62
|
+
if "messages" in record and isinstance(record["messages"], list):
|
|
63
|
+
return " ".join(str(m.get("content", "")) for m in record["messages"])
|
|
64
|
+
parts = [str(v) for v in record.values()
|
|
65
|
+
if isinstance(v, (str, int, float)) and str(v).strip()]
|
|
66
|
+
return " ".join(parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Shared streaming infrastructure ───────────────────────────────────
|
|
70
|
+
|
|
71
|
+
class _StreamingJSONLBase(Dataset):
|
|
72
|
+
"""Base class providing byte-offset index + lazy per-process file handle."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, data_path: str):
|
|
75
|
+
self.data_path = os.path.abspath(data_path)
|
|
76
|
+
self.offsets: list[int] = []
|
|
77
|
+
with open(self.data_path, "rb") as f:
|
|
78
|
+
pos = 0
|
|
79
|
+
while True:
|
|
80
|
+
line = f.readline()
|
|
81
|
+
if not line:
|
|
82
|
+
break
|
|
83
|
+
if line.strip():
|
|
84
|
+
self.offsets.append(pos)
|
|
85
|
+
pos = f.tell()
|
|
86
|
+
if not self.offsets:
|
|
87
|
+
raise ValueError(f"No JSONL records in {data_path}")
|
|
88
|
+
self._fh = None
|
|
89
|
+
self._fh_lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
def _get_fh(self):
|
|
92
|
+
pid = os.getpid()
|
|
93
|
+
if self._fh is None or getattr(self._fh, "_chronos_pid", None) != pid:
|
|
94
|
+
if self._fh is not None:
|
|
95
|
+
try:
|
|
96
|
+
self._fh.close()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
self._fh = open(self.data_path, "rb")
|
|
100
|
+
self._fh._chronos_pid = pid
|
|
101
|
+
return self._fh
|
|
102
|
+
|
|
103
|
+
def _read_record(self, index: int) -> dict:
|
|
104
|
+
offset = self.offsets[index]
|
|
105
|
+
with self._fh_lock:
|
|
106
|
+
fh = self._get_fh()
|
|
107
|
+
fh.seek(offset)
|
|
108
|
+
raw = fh.readline()
|
|
109
|
+
return json.loads(raw.decode("utf-8"))
|
|
110
|
+
|
|
111
|
+
def __len__(self):
|
|
112
|
+
return len(self.offsets)
|
|
113
|
+
|
|
114
|
+
def __del__(self):
|
|
115
|
+
try:
|
|
116
|
+
if self._fh is not None:
|
|
117
|
+
self._fh.close()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── Pretrain / generic LM ────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
class FlexibleDataset(_StreamingJSONLBase):
|
|
125
|
+
"""Pretrain-style streaming dataset; auto-detects the text field."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, data_path: str, tokenizer, max_length: int = 512):
|
|
128
|
+
super().__init__(data_path)
|
|
129
|
+
self.tokenizer = tokenizer
|
|
130
|
+
self.max_length = max_length
|
|
131
|
+
first = self._read_record(0)
|
|
132
|
+
detected = _extract_text(first)
|
|
133
|
+
field_hint = (
|
|
134
|
+
next((k for k in _TEXT_KEYS if k in first), None)
|
|
135
|
+
or next((f"{k1}+{k2}" for k1, k2 in _PAIR_KEYS
|
|
136
|
+
if k1 in first and k2 in first), None)
|
|
137
|
+
or ("conversations" if "conversations" in first else "auto")
|
|
138
|
+
)
|
|
139
|
+
print(f"[FlexibleDataset] {len(self.offsets)} records (streaming), "
|
|
140
|
+
f"detected format: '{field_hint}', "
|
|
141
|
+
f"sample preview: {detected[:80]!r}")
|
|
142
|
+
|
|
143
|
+
def __getitem__(self, index):
|
|
144
|
+
record = self._read_record(index)
|
|
145
|
+
text = _extract_text(record)
|
|
146
|
+
tok = self.tokenizer(
|
|
147
|
+
text, add_special_tokens=False,
|
|
148
|
+
max_length=self.max_length - 2, truncation=True,
|
|
149
|
+
)
|
|
150
|
+
tokens = (
|
|
151
|
+
[self.tokenizer.bos_token_id]
|
|
152
|
+
+ tok.input_ids
|
|
153
|
+
+ [self.tokenizer.eos_token_id]
|
|
154
|
+
)
|
|
155
|
+
pad_id = self.tokenizer.pad_token_id or 0
|
|
156
|
+
tokens = tokens + [pad_id] * (self.max_length - len(tokens))
|
|
157
|
+
input_ids = torch.tensor(tokens, dtype=torch.long)
|
|
158
|
+
labels = input_ids.clone()
|
|
159
|
+
labels[input_ids == pad_id] = -100
|
|
160
|
+
return input_ids, labels
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── SFT (conversations → assistant-token loss mask) ─────────────────
|
|
164
|
+
|
|
165
|
+
class StreamingSFTDataset(_StreamingJSONLBase):
|
|
166
|
+
"""Streaming counterpart to minimind's SFTDataset.
|
|
167
|
+
|
|
168
|
+
Reads `conversations: [{role, content, ...}, ...]` records, applies
|
|
169
|
+
the tokenizer's chat template, and masks every position outside an
|
|
170
|
+
assistant turn with -100 so the loss only fires on assistant tokens.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, data_path: str, tokenizer, max_length: int = 1024):
|
|
174
|
+
super().__init__(data_path)
|
|
175
|
+
self.tokenizer = tokenizer
|
|
176
|
+
self.max_length = max_length
|
|
177
|
+
# Sentinels marking the assistant turn boundary in encoded form.
|
|
178
|
+
# Same formulation minimind uses (`bos`+'assistant\n' / `eos`+'\n').
|
|
179
|
+
self.bos_id = tokenizer(
|
|
180
|
+
f'{tokenizer.bos_token}assistant\n', add_special_tokens=False
|
|
181
|
+
).input_ids
|
|
182
|
+
self.eos_id = tokenizer(
|
|
183
|
+
f'{tokenizer.eos_token}\n', add_special_tokens=False
|
|
184
|
+
).input_ids
|
|
185
|
+
print(f"[StreamingSFTDataset] {len(self.offsets)} records (streaming)")
|
|
186
|
+
|
|
187
|
+
def _build_prompt(self, conversations) -> str:
|
|
188
|
+
messages = []
|
|
189
|
+
tools = None
|
|
190
|
+
for m in conversations:
|
|
191
|
+
m = dict(m)
|
|
192
|
+
if m.get("role") == "system" and m.get("tools"):
|
|
193
|
+
tools = json.loads(m["tools"]) if isinstance(m["tools"], str) else m["tools"]
|
|
194
|
+
if m.get("tool_calls") and isinstance(m["tool_calls"], str):
|
|
195
|
+
m["tool_calls"] = json.loads(m["tool_calls"])
|
|
196
|
+
messages.append(m)
|
|
197
|
+
return self.tokenizer.apply_chat_template(
|
|
198
|
+
messages, tokenize=False, add_generation_prompt=False, tools=tools,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _label_mask(self, input_ids: list[int]) -> list[int]:
|
|
202
|
+
"""-100 outside every <bos>assistant\\n ... <eos>\\n span."""
|
|
203
|
+
labels = [-100] * len(input_ids)
|
|
204
|
+
i = 0
|
|
205
|
+
nb = len(self.bos_id)
|
|
206
|
+
ne = len(self.eos_id)
|
|
207
|
+
while i < len(input_ids):
|
|
208
|
+
if input_ids[i:i + nb] == self.bos_id:
|
|
209
|
+
start = i + nb
|
|
210
|
+
end = start
|
|
211
|
+
while end < len(input_ids):
|
|
212
|
+
if input_ids[end:end + ne] == self.eos_id:
|
|
213
|
+
break
|
|
214
|
+
end += 1
|
|
215
|
+
for j in range(start, min(end + ne, self.max_length)):
|
|
216
|
+
labels[j] = input_ids[j]
|
|
217
|
+
i = end + ne if end < len(input_ids) else len(input_ids)
|
|
218
|
+
else:
|
|
219
|
+
i += 1
|
|
220
|
+
return labels
|
|
221
|
+
|
|
222
|
+
def __getitem__(self, index):
|
|
223
|
+
record = self._read_record(index)
|
|
224
|
+
convs = record.get("conversations")
|
|
225
|
+
if not convs:
|
|
226
|
+
# Graceful fall-through for mixed corpora — treat as pretrain.
|
|
227
|
+
text = _extract_text(record)
|
|
228
|
+
ids = self.tokenizer(text, truncation=True,
|
|
229
|
+
max_length=self.max_length).input_ids
|
|
230
|
+
else:
|
|
231
|
+
prompt = self._build_prompt(convs)
|
|
232
|
+
ids = self.tokenizer(prompt).input_ids[:self.max_length]
|
|
233
|
+
pad_id = self.tokenizer.pad_token_id or 0
|
|
234
|
+
ids = ids + [pad_id] * (self.max_length - len(ids))
|
|
235
|
+
labels = self._label_mask(ids)
|
|
236
|
+
return (
|
|
237
|
+
torch.tensor(ids, dtype=torch.long),
|
|
238
|
+
torch.tensor(labels, dtype=torch.long),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ── DPO / ORPO (chosen + rejected pair) ─────────────────────────────
|
|
243
|
+
|
|
244
|
+
class StreamingDPODataset(_StreamingJSONLBase):
|
|
245
|
+
"""Streaming counterpart to minimind's DPODataset.
|
|
246
|
+
|
|
247
|
+
Returns the dict shape the DPO/ORPO trainers expect:
|
|
248
|
+
{x_chosen, y_chosen, mask_chosen, x_rejected, y_rejected, mask_rejected}
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, data_path: str, tokenizer, max_length: int = 4096):
|
|
252
|
+
super().__init__(data_path)
|
|
253
|
+
self.tokenizer = tokenizer
|
|
254
|
+
self.max_length = max_length
|
|
255
|
+
self.padding = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0
|
|
256
|
+
self.bos_id = tokenizer(
|
|
257
|
+
f'{tokenizer.bos_token}assistant\n', add_special_tokens=False
|
|
258
|
+
).input_ids
|
|
259
|
+
self.eos_id = tokenizer(
|
|
260
|
+
f'{tokenizer.eos_token}\n', add_special_tokens=False
|
|
261
|
+
).input_ids
|
|
262
|
+
print(f"[StreamingDPODataset] {len(self.offsets)} pairs (streaming)")
|
|
263
|
+
|
|
264
|
+
def _loss_mask(self, ids: list[int]) -> list[int]:
|
|
265
|
+
mask = [0] * len(ids)
|
|
266
|
+
i = 0
|
|
267
|
+
nb = len(self.bos_id); ne = len(self.eos_id)
|
|
268
|
+
while i < len(ids):
|
|
269
|
+
if ids[i:i + nb] == self.bos_id:
|
|
270
|
+
start = i + nb
|
|
271
|
+
end = start
|
|
272
|
+
while end < len(ids):
|
|
273
|
+
if ids[end:end + ne] == self.eos_id:
|
|
274
|
+
break
|
|
275
|
+
end += 1
|
|
276
|
+
for j in range(start, min(end + ne, self.max_length)):
|
|
277
|
+
mask[j] = 1
|
|
278
|
+
i = end + ne if end < len(ids) else len(ids)
|
|
279
|
+
else:
|
|
280
|
+
i += 1
|
|
281
|
+
return mask
|
|
282
|
+
|
|
283
|
+
def __getitem__(self, index):
|
|
284
|
+
rec = self._read_record(index)
|
|
285
|
+
chosen = rec["chosen"]; rejected = rec["rejected"]
|
|
286
|
+
|
|
287
|
+
chosen_prompt = self.tokenizer.apply_chat_template(
|
|
288
|
+
chosen, tokenize=False, add_generation_prompt=False,
|
|
289
|
+
)
|
|
290
|
+
rejected_prompt = self.tokenizer.apply_chat_template(
|
|
291
|
+
rejected, tokenize=False, add_generation_prompt=False,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
c_enc = self.tokenizer(chosen_prompt, truncation=True,
|
|
295
|
+
max_length=self.max_length, padding="max_length")
|
|
296
|
+
r_enc = self.tokenizer(rejected_prompt, truncation=True,
|
|
297
|
+
max_length=self.max_length, padding="max_length")
|
|
298
|
+
c_ids = c_enc["input_ids"]
|
|
299
|
+
r_ids = r_enc["input_ids"]
|
|
300
|
+
c_mask = self._loss_mask(c_ids)
|
|
301
|
+
r_mask = self._loss_mask(r_ids)
|
|
302
|
+
return {
|
|
303
|
+
"x_chosen": torch.tensor(c_ids[:-1], dtype=torch.long),
|
|
304
|
+
"y_chosen": torch.tensor(c_ids[1:], dtype=torch.long),
|
|
305
|
+
"mask_chosen": torch.tensor(c_mask[1:], dtype=torch.long),
|
|
306
|
+
"x_rejected": torch.tensor(r_ids[:-1], dtype=torch.long),
|
|
307
|
+
"y_rejected": torch.tensor(r_ids[1:], dtype=torch.long),
|
|
308
|
+
"mask_rejected":torch.tensor(r_mask[1:], dtype=torch.long),
|
|
309
|
+
}
|
chronos/deps.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/deps.py — Automatic dependency bootstrap for minimind-master kernel.
|
|
3
|
+
|
|
4
|
+
When minimind-master is not found locally, this module downloads it from
|
|
5
|
+
GitHub and makes it importable. Called automatically on first import of
|
|
6
|
+
any chronos.model module.
|
|
7
|
+
|
|
8
|
+
Legal notice: minimind is licensed under Apache-2.0.
|
|
9
|
+
See THIRD_PARTY_NOTICES.md for full attribution.
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import importlib
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
MINIMIND_REPO = "https://github.com/jingyaogong/minimind"
|
|
18
|
+
MINIMIND_COMMIT = "master" # pin to a tag/commit for reproducibility
|
|
19
|
+
|
|
20
|
+
# Candidate local paths (in priority order)
|
|
21
|
+
_CANDIDATE_PATHS = [
|
|
22
|
+
Path(__file__).parent.parent.parent / "minimind-master", # sibling dir
|
|
23
|
+
Path(__file__).parent.parent / "minimind-master",
|
|
24
|
+
Path.home() / ".cache" / "chronos" / "minimind-master",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _find_local() -> Path | None:
|
|
29
|
+
for p in _CANDIDATE_PATHS:
|
|
30
|
+
if (p / "model" / "model_minimind.py").exists():
|
|
31
|
+
return p
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _download(target: Path) -> Path:
|
|
36
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
print(f"[Chronos] minimind not found locally. Downloading from {MINIMIND_REPO} ...")
|
|
38
|
+
print(f"[Chronos] Target: {target}")
|
|
39
|
+
print(f"[Chronos] License: Apache-2.0 | Copyright jingyaogong")
|
|
40
|
+
subprocess.run(
|
|
41
|
+
["git", "clone", "--depth", "1", "--branch", MINIMIND_COMMIT,
|
|
42
|
+
MINIMIND_REPO, str(target)],
|
|
43
|
+
check=True,
|
|
44
|
+
)
|
|
45
|
+
print(f"[Chronos] minimind downloaded to {target}")
|
|
46
|
+
return target
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ensure_minimind() -> Path:
|
|
50
|
+
"""
|
|
51
|
+
Ensure minimind-master is available and on sys.path.
|
|
52
|
+
Returns the path to the minimind root directory.
|
|
53
|
+
"""
|
|
54
|
+
local = _find_local()
|
|
55
|
+
if local is None:
|
|
56
|
+
cache_dir = Path.home() / ".cache" / "chronos" / "minimind-master"
|
|
57
|
+
local = _download(cache_dir)
|
|
58
|
+
|
|
59
|
+
root = str(local)
|
|
60
|
+
if root not in sys.path:
|
|
61
|
+
sys.path.insert(0, root)
|
|
62
|
+
return local
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_tokenizer_path() -> str:
|
|
66
|
+
"""Return the path to the minimind tokenizer directory."""
|
|
67
|
+
root = ensure_minimind()
|
|
68
|
+
return str(root / "model")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Run on import
|
|
72
|
+
_minimind_root = ensure_minimind()
|
chronos/eval/__init__.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/eval/benchmark.py
|
|
3
|
+
|
|
4
|
+
Phase 3 end-to-end evaluation:
|
|
5
|
+
- Perplexity on held-out text (accuracy proxy)
|
|
6
|
+
- Throughput (tokens/s) with and without async prefetch
|
|
7
|
+
- KV cache memory usage comparison: standard vs hybrid attention
|
|
8
|
+
- Soft gating fallback rate
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python -m chronos.eval.benchmark \
|
|
12
|
+
--model_path ./out/chronos_512_moe.pth \
|
|
13
|
+
--data_path ./dataset/eval.jsonl \
|
|
14
|
+
--device cpu \
|
|
15
|
+
--max_new_tokens 64
|
|
16
|
+
"""
|
|
17
|
+
import sys
|
|
18
|
+
import chronos.deps # ensure minimind on sys.path
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import math
|
|
22
|
+
import time
|
|
23
|
+
import torch
|
|
24
|
+
import torch.nn.functional as F
|
|
25
|
+
from torch.utils.data import DataLoader
|
|
26
|
+
from transformers import AutoTokenizer
|
|
27
|
+
|
|
28
|
+
from chronos.model.config import ChronosConfig
|
|
29
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
30
|
+
from chronos.model.hybrid_attention import MLAAttention, SlidingWindowAttention
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compute_perplexity(model, dataloader, device, max_batches=50):
|
|
34
|
+
model.eval()
|
|
35
|
+
total_loss, total_tokens = 0.0, 0
|
|
36
|
+
with torch.no_grad():
|
|
37
|
+
for i, (input_ids, labels) in enumerate(dataloader):
|
|
38
|
+
if i >= max_batches:
|
|
39
|
+
break
|
|
40
|
+
input_ids, labels = input_ids.to(device), labels.to(device)
|
|
41
|
+
out, _ = model(input_ids, labels=labels)
|
|
42
|
+
n_tokens = (labels != -100).sum().item()
|
|
43
|
+
total_loss += out.loss.item() * n_tokens
|
|
44
|
+
total_tokens += n_tokens
|
|
45
|
+
ppl = math.exp(total_loss / max(total_tokens, 1))
|
|
46
|
+
return ppl
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def measure_throughput(model, input_ids, device, max_new_tokens=64):
|
|
50
|
+
model.eval()
|
|
51
|
+
input_ids = input_ids.to(device)
|
|
52
|
+
t0 = time.monotonic()
|
|
53
|
+
with torch.no_grad():
|
|
54
|
+
out, lp = model(input_ids, use_cache=True)
|
|
55
|
+
past_kv = out.past_key_values
|
|
56
|
+
for _ in range(max_new_tokens):
|
|
57
|
+
x = torch.randint(0, model.config.vocab_size, (1, 1), device=device)
|
|
58
|
+
out2, _ = model(x, past_key_values=past_kv, use_cache=True)
|
|
59
|
+
past_kv = out2.past_key_values
|
|
60
|
+
elapsed = time.monotonic() - t0
|
|
61
|
+
return max_new_tokens / elapsed
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def kv_cache_memory_bytes(past_key_values, model):
|
|
65
|
+
"""Estimate total KV cache memory in bytes (fp32)."""
|
|
66
|
+
total = 0
|
|
67
|
+
for i, kv in enumerate(past_key_values):
|
|
68
|
+
if kv is None:
|
|
69
|
+
continue
|
|
70
|
+
for t in kv:
|
|
71
|
+
total += t.numel() * t.element_size()
|
|
72
|
+
return total
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_benchmark(args):
|
|
76
|
+
config = ChronosConfig(
|
|
77
|
+
hidden_size=args.hidden_size,
|
|
78
|
+
num_hidden_layers=args.num_hidden_layers,
|
|
79
|
+
num_experts=args.num_experts,
|
|
80
|
+
use_moe=True,
|
|
81
|
+
use_hybrid_attention=True,
|
|
82
|
+
kv_latent_dim=args.kv_latent_dim,
|
|
83
|
+
sliding_window_size=args.sliding_window_size,
|
|
84
|
+
vram_budget_gb=args.vram_budget_gb,
|
|
85
|
+
)
|
|
86
|
+
model = ChronosForCausalLM(config).to(args.device)
|
|
87
|
+
|
|
88
|
+
if args.model_path:
|
|
89
|
+
weights = torch.load(args.model_path, map_location=args.device)
|
|
90
|
+
model.load_state_dict(weights, strict=False)
|
|
91
|
+
print(f"Loaded: {args.model_path}")
|
|
92
|
+
|
|
93
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
94
|
+
chronos.deps.get_tokenizer_path()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
from dataset.lm_dataset import PretrainDataset
|
|
98
|
+
dataset = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
|
99
|
+
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False)
|
|
100
|
+
|
|
101
|
+
print("\n=== Phase 3 Benchmark ===")
|
|
102
|
+
|
|
103
|
+
# 1. Perplexity
|
|
104
|
+
ppl = compute_perplexity(model, loader, args.device)
|
|
105
|
+
print(f"Perplexity: {ppl:.2f}")
|
|
106
|
+
|
|
107
|
+
# 2. Throughput
|
|
108
|
+
sample_ids = torch.randint(0, config.vocab_size, (1, 32))
|
|
109
|
+
tps = measure_throughput(model, sample_ids, args.device, args.max_new_tokens)
|
|
110
|
+
print(f"Throughput: {tps:.1f} tokens/s")
|
|
111
|
+
|
|
112
|
+
# 3. KV cache memory after N decode steps
|
|
113
|
+
model.eval()
|
|
114
|
+
with torch.no_grad():
|
|
115
|
+
x = torch.randint(0, config.vocab_size, (1, 32), device=args.device)
|
|
116
|
+
out, _ = model(x, use_cache=True)
|
|
117
|
+
past_kv = out.past_key_values
|
|
118
|
+
for _ in range(args.max_new_tokens):
|
|
119
|
+
xn = torch.randint(0, config.vocab_size, (1, 1), device=args.device)
|
|
120
|
+
out2, _ = model(xn, past_key_values=past_kv, use_cache=True)
|
|
121
|
+
past_kv = out2.past_key_values
|
|
122
|
+
|
|
123
|
+
kv_bytes = kv_cache_memory_bytes(past_kv, model)
|
|
124
|
+
print(f"KV cache after {args.max_new_tokens} steps: {kv_bytes/1024:.1f} KB")
|
|
125
|
+
|
|
126
|
+
# 4. Layer-wise cache sizes
|
|
127
|
+
print("\nLayer-wise KV cache:")
|
|
128
|
+
for i, kv in enumerate(past_kv):
|
|
129
|
+
if kv is not None:
|
|
130
|
+
layer_type = type(model.model.layers[i].self_attn).__name__
|
|
131
|
+
seq_len = kv[0].shape[1]
|
|
132
|
+
print(f" Layer {i:2d} ({layer_type:25s}): seq_len={seq_len:4d} "
|
|
133
|
+
f"bytes={sum(t.numel()*t.element_size() for t in kv)//1024}KB")
|
|
134
|
+
|
|
135
|
+
print(f"\n=== Summary ===")
|
|
136
|
+
print(f" PPL={ppl:.2f} TPS={tps:.1f} KV={kv_bytes//1024}KB "
|
|
137
|
+
f"hybrid_attn=True window={config.sliding_window_size}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
parser = argparse.ArgumentParser()
|
|
142
|
+
parser.add_argument("--model_path", type=str, default=None)
|
|
143
|
+
parser.add_argument("--data_path", type=str, required=True)
|
|
144
|
+
parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu")
|
|
145
|
+
parser.add_argument("--batch_size", type=int, default=4)
|
|
146
|
+
parser.add_argument("--max_seq_len", type=int, default=256)
|
|
147
|
+
parser.add_argument("--max_new_tokens", type=int, default=64)
|
|
148
|
+
parser.add_argument("--hidden_size", type=int, default=512)
|
|
149
|
+
parser.add_argument("--num_hidden_layers", type=int, default=8)
|
|
150
|
+
parser.add_argument("--num_experts", type=int, default=4)
|
|
151
|
+
parser.add_argument("--kv_latent_dim", type=int, default=64)
|
|
152
|
+
parser.add_argument("--sliding_window_size", type=int, default=2048)
|
|
153
|
+
parser.add_argument("--vram_budget_gb", type=float, default=4.0)
|
|
154
|
+
args = parser.parse_args()
|
|
155
|
+
run_benchmark(args)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|