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
ui/tabs/inference_tab.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ui/tabs/inference_tab.py — Real-time generation with tokens/s display
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from ui.gradio_compat import gr
|
|
7
|
+
|
|
8
|
+
import chronos.deps # auto-bootstrap minimind on sys.path
|
|
9
|
+
from ui.i18n import t, register_translatable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
INFERENCE_BACKEND_CHOICES = ("mlx", "cuda", "xpu", "mps", "cpu")
|
|
13
|
+
TORCH_INFERENCE_PRIORITY = ("cuda", "xpu", "mps", "cpu")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _best_torch_inference_backend() -> str:
|
|
17
|
+
from chronos.backend import BackendDispatcher
|
|
18
|
+
|
|
19
|
+
d = BackendDispatcher()
|
|
20
|
+
for name in TORCH_INFERENCE_PRIORITY:
|
|
21
|
+
if d.info(name).available:
|
|
22
|
+
return name
|
|
23
|
+
return "cpu"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _available_inference_backend_choices() -> list[str]:
|
|
27
|
+
from chronos.backend import available
|
|
28
|
+
|
|
29
|
+
detected = set(available())
|
|
30
|
+
return ["auto"] + [name for name in INFERENCE_BACKEND_CHOICES if name in detected]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_inference_backend_value() -> str:
|
|
34
|
+
return "auto"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_inference_backend(requested_backend: str, model_path_val: str, sniffed: dict) -> tuple[str, str]:
|
|
38
|
+
from chronos.backend import select
|
|
39
|
+
|
|
40
|
+
requested = (requested_backend or "auto").strip().lower() or "auto"
|
|
41
|
+
backend = select(None if requested == "auto" else requested)
|
|
42
|
+
note = ""
|
|
43
|
+
|
|
44
|
+
if requested not in {"", "auto"} and backend != requested:
|
|
45
|
+
note = (
|
|
46
|
+
f"[backend fallback] Requested {requested}, but it is not available; "
|
|
47
|
+
f"using {backend}.\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Current MLX inference in this repo does not have a lossless
|
|
51
|
+
# PyTorch-.pth loader. If we detected a standard Chronos checkpoint,
|
|
52
|
+
# prefer a torch backend so the weights actually load.
|
|
53
|
+
if backend == "mlx" and sniffed:
|
|
54
|
+
fallback = _best_torch_inference_backend()
|
|
55
|
+
if fallback != "mlx":
|
|
56
|
+
backend = fallback
|
|
57
|
+
note = note + (
|
|
58
|
+
f"[backend fallback] Detected a PyTorch Chronos checkpoint at {model_path_val}; "
|
|
59
|
+
f"using {backend} because the current MLX .pth import path is not lossless.\n"
|
|
60
|
+
)
|
|
61
|
+
return backend, note
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _checkpoint_expert_cache_dir(model_path_val: str, model_cfg) -> str:
|
|
65
|
+
"""Keep lazy expert shards isolated per checkpoint.
|
|
66
|
+
|
|
67
|
+
Reusing a single ./expert_cache across different .pth files can silently
|
|
68
|
+
load stale expert shards. The cache key uses path + stat metadata + core
|
|
69
|
+
topology so repeated runs reuse the same shards, while changed checkpoints
|
|
70
|
+
get a clean cache namespace.
|
|
71
|
+
"""
|
|
72
|
+
import hashlib
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
base = os.path.join(os.getcwd(), "expert_cache")
|
|
76
|
+
if not model_path_val or not os.path.exists(model_path_val):
|
|
77
|
+
return base
|
|
78
|
+
try:
|
|
79
|
+
st = os.stat(model_path_val)
|
|
80
|
+
except OSError:
|
|
81
|
+
return base
|
|
82
|
+
topology = (
|
|
83
|
+
getattr(model_cfg, "vocab_size", ""),
|
|
84
|
+
getattr(model_cfg, "hidden_size", ""),
|
|
85
|
+
getattr(model_cfg, "num_hidden_layers", ""),
|
|
86
|
+
getattr(model_cfg, "num_experts", ""),
|
|
87
|
+
getattr(model_cfg, "moe_intermediate_size", ""),
|
|
88
|
+
)
|
|
89
|
+
raw = f"{os.path.abspath(model_path_val)}:{st.st_size}:{st.st_mtime_ns}:{topology}"
|
|
90
|
+
key = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
|
|
91
|
+
return os.path.join(base, f"ckpt_{key}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _replace_torch_experts_with_placeholders(model, model_cfg) -> None:
|
|
95
|
+
"""Remove randomly initialised live experts before model.to(device).
|
|
96
|
+
|
|
97
|
+
For .pth inference the real expert tensors are sharded from the checkpoint
|
|
98
|
+
to SSD by ChronosInferenceEngine.setup_from_state_dict(). Keeping the
|
|
99
|
+
constructor's random experts alive until model.to(mps/cuda) defeats the
|
|
100
|
+
SSD+DRAM lazy-load design by moving full dummy experts onto the device.
|
|
101
|
+
"""
|
|
102
|
+
import torch
|
|
103
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward, LazyExpertPlaceholder
|
|
104
|
+
|
|
105
|
+
layers = getattr(getattr(model, "model", None), "layers", [])
|
|
106
|
+
hidden = int(getattr(model_cfg, "hidden_size", 0) or 0)
|
|
107
|
+
intermediate = int(getattr(model_cfg, "moe_intermediate_size", 0) or 0)
|
|
108
|
+
if not hidden or not intermediate:
|
|
109
|
+
return
|
|
110
|
+
for layer in layers:
|
|
111
|
+
moe = getattr(layer, "mlp", None)
|
|
112
|
+
if not isinstance(moe, ChronosMOEFeedForward):
|
|
113
|
+
continue
|
|
114
|
+
for idx in range(len(moe.experts)):
|
|
115
|
+
moe.experts[idx] = LazyExpertPlaceholder(hidden, intermediate, torch.float16)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_torch_inference(backend: str, model_cfg, model_path_val: str, token_ids: list[int],
|
|
119
|
+
max_tok: int, temp: float, eos_token_id: int | None) -> list[int]:
|
|
120
|
+
import os
|
|
121
|
+
import torch
|
|
122
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
123
|
+
from chronos.runtime.inference_engine import ChronosInferenceEngine
|
|
124
|
+
|
|
125
|
+
model = ChronosForCausalLM(model_cfg)
|
|
126
|
+
weights = None
|
|
127
|
+
if model_path_val and os.path.exists(model_path_val):
|
|
128
|
+
weights = torch.load(model_path_val, map_location="cpu")
|
|
129
|
+
base_only = {
|
|
130
|
+
k: v for k, v in weights.items()
|
|
131
|
+
if ".mlp.experts." not in k
|
|
132
|
+
}
|
|
133
|
+
model.load_state_dict(base_only, strict=False)
|
|
134
|
+
_replace_torch_experts_with_placeholders(model, model_cfg)
|
|
135
|
+
|
|
136
|
+
device_map = {"cuda": "cuda", "xpu": "xpu", "mps": "mps", "cpu": "cpu"}[backend]
|
|
137
|
+
model = model.to(device_map).eval()
|
|
138
|
+
input_ids = torch.tensor([token_ids]).to(device_map)
|
|
139
|
+
engine = ChronosInferenceEngine(
|
|
140
|
+
model,
|
|
141
|
+
model_cfg,
|
|
142
|
+
ssd_dir=_checkpoint_expert_cache_dir(model_path_val, model_cfg),
|
|
143
|
+
)
|
|
144
|
+
if weights is not None:
|
|
145
|
+
engine.setup_from_state_dict(weights, warm_expert_ids=[])
|
|
146
|
+
del weights
|
|
147
|
+
weights = None
|
|
148
|
+
import gc
|
|
149
|
+
gc.collect()
|
|
150
|
+
else:
|
|
151
|
+
engine.setup(warm_expert_ids=[])
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
out = engine.generate(
|
|
155
|
+
input_ids,
|
|
156
|
+
max_new_tokens=int(max_tok),
|
|
157
|
+
temperature=float(temp),
|
|
158
|
+
eos_token_id=eos_token_id,
|
|
159
|
+
)
|
|
160
|
+
finally:
|
|
161
|
+
engine.teardown()
|
|
162
|
+
|
|
163
|
+
prompt_len = input_ids.shape[1]
|
|
164
|
+
generated = out[0, prompt_len:].tolist()
|
|
165
|
+
|
|
166
|
+
return generated
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def build_inference_tab(config_state: gr.State):
|
|
170
|
+
with gr.Tab(t("tab.inference")) as tab:
|
|
171
|
+
register_translatable(tab, "tab.inference")
|
|
172
|
+
|
|
173
|
+
model_path = gr.Textbox(
|
|
174
|
+
label=t("infer.model_path"), placeholder="./out/chronos_512_moe.pth"
|
|
175
|
+
)
|
|
176
|
+
register_translatable(model_path, "infer.model_path")
|
|
177
|
+
|
|
178
|
+
prompt = gr.Textbox(
|
|
179
|
+
label=t("infer.prompt"), lines=4,
|
|
180
|
+
placeholder="Explain how Chronos handles expert prefetch during prefill..."
|
|
181
|
+
)
|
|
182
|
+
register_translatable(prompt, "infer.prompt")
|
|
183
|
+
|
|
184
|
+
with gr.Row():
|
|
185
|
+
inference_backend = gr.Dropdown(
|
|
186
|
+
choices=_available_inference_backend_choices(),
|
|
187
|
+
value=_default_inference_backend_value(),
|
|
188
|
+
label=t("infer.backend"),
|
|
189
|
+
)
|
|
190
|
+
max_tokens = gr.Slider(16, 512, value=128, step=16, label=t("infer.max_tokens"))
|
|
191
|
+
temperature = gr.Slider(0.1, 2.0, value=0.85, step=0.05, label=t("infer.temperature"))
|
|
192
|
+
register_translatable(inference_backend, "infer.backend")
|
|
193
|
+
register_translatable(max_tokens, "infer.max_tokens")
|
|
194
|
+
register_translatable(temperature, "infer.temperature")
|
|
195
|
+
|
|
196
|
+
gen_btn = gr.Button(t("infer.generate"), variant="primary")
|
|
197
|
+
register_translatable(gen_btn, "infer.generate")
|
|
198
|
+
|
|
199
|
+
output_box = gr.Textbox(label=t("infer.output"), lines=10, interactive=False)
|
|
200
|
+
tps_box = gr.Number(label=t("infer.tps"), value=0.0, interactive=False, precision=1)
|
|
201
|
+
register_translatable(output_box, "infer.output")
|
|
202
|
+
register_translatable(tps_box, "infer.tps")
|
|
203
|
+
|
|
204
|
+
def _sniff_checkpoint(path: str) -> dict:
|
|
205
|
+
"""Inspect a Chronos .pth and return the topology fields encoded
|
|
206
|
+
in the saved tensor shapes. Returns {} if the file is unreadable
|
|
207
|
+
or doesn't look like a Chronos checkpoint.
|
|
208
|
+
|
|
209
|
+
Sniffed fields:
|
|
210
|
+
vocab_size, hidden_size, num_hidden_layers, num_experts,
|
|
211
|
+
moe_intermediate_size, num_attention_heads, num_key_value_heads.
|
|
212
|
+
"""
|
|
213
|
+
import os as _os
|
|
214
|
+
if not path or not _os.path.exists(path):
|
|
215
|
+
return {}
|
|
216
|
+
try:
|
|
217
|
+
import torch as _t
|
|
218
|
+
sd = _t.load(path, map_location="cpu")
|
|
219
|
+
if not isinstance(sd, dict):
|
|
220
|
+
return {}
|
|
221
|
+
out = {}
|
|
222
|
+
|
|
223
|
+
embed = sd.get("model.embed_tokens.weight")
|
|
224
|
+
if embed is not None:
|
|
225
|
+
out["vocab_size"], out["hidden_size"] = int(embed.shape[0]), int(embed.shape[1])
|
|
226
|
+
|
|
227
|
+
# Count layers via highest layer index in keys.
|
|
228
|
+
layer_idxs = set()
|
|
229
|
+
for k in sd.keys():
|
|
230
|
+
if k.startswith("model.layers."):
|
|
231
|
+
try:
|
|
232
|
+
layer_idxs.add(int(k.split(".")[2]))
|
|
233
|
+
except (ValueError, IndexError):
|
|
234
|
+
pass
|
|
235
|
+
if layer_idxs:
|
|
236
|
+
out["num_hidden_layers"] = max(layer_idxs) + 1
|
|
237
|
+
|
|
238
|
+
# Count experts on layer 0.
|
|
239
|
+
expert_idxs = set()
|
|
240
|
+
for k in sd.keys():
|
|
241
|
+
if k.startswith("model.layers.0.mlp.experts."):
|
|
242
|
+
try:
|
|
243
|
+
expert_idxs.add(int(k.split(".")[5]))
|
|
244
|
+
except (ValueError, IndexError):
|
|
245
|
+
pass
|
|
246
|
+
if expert_idxs:
|
|
247
|
+
out["num_experts"] = max(expert_idxs) + 1
|
|
248
|
+
|
|
249
|
+
# Per-expert FFN width from gate_proj shape (intermediate, hidden).
|
|
250
|
+
gate = sd.get("model.layers.0.mlp.experts.0.gate_proj.weight")
|
|
251
|
+
if gate is not None:
|
|
252
|
+
out["moe_intermediate_size"] = int(gate.shape[0])
|
|
253
|
+
|
|
254
|
+
# Attention head counts from q/k_proj shapes.
|
|
255
|
+
qw = sd.get("model.layers.0.self_attn.q_proj.weight")
|
|
256
|
+
kw = sd.get("model.layers.0.self_attn.k_proj.weight")
|
|
257
|
+
if qw is not None and kw is not None and out.get("hidden_size"):
|
|
258
|
+
h = out["hidden_size"]
|
|
259
|
+
# qw: (n_heads * head_dim, hidden); kw: (n_kv * head_dim, hidden)
|
|
260
|
+
# head_dim is typically hidden_size // n_heads, but we can't
|
|
261
|
+
# split q without knowing head_dim. Conservative: assume the
|
|
262
|
+
# ratio matches MiniMind defaults (head_dim = h // num_heads).
|
|
263
|
+
# Solve via the kw / qw ratio assuming both share head_dim.
|
|
264
|
+
out["num_attention_heads_total_dim"] = int(qw.shape[0])
|
|
265
|
+
out["num_kv_heads_total_dim"] = int(kw.shape[0])
|
|
266
|
+
|
|
267
|
+
# MLA attention uses split query and latent KV projections:
|
|
268
|
+
# q_nope_proj: (n_heads * nope_dim, hidden)
|
|
269
|
+
# q_rope_proj: (n_heads * rope_dim, hidden)
|
|
270
|
+
# kv_down_proj: (kv_latent_dim, hidden)
|
|
271
|
+
# v_proj: (n_kv_heads * head_dim, kv_latent_dim)
|
|
272
|
+
qnope = sd.get("model.layers.0.self_attn.q_nope_proj.weight")
|
|
273
|
+
qrope = sd.get("model.layers.0.self_attn.q_rope_proj.weight")
|
|
274
|
+
kvdown = sd.get("model.layers.0.self_attn.kv_down_proj.weight")
|
|
275
|
+
vproj = sd.get("model.layers.0.self_attn.v_proj.weight")
|
|
276
|
+
if qnope is not None and qrope is not None and out.get("hidden_size"):
|
|
277
|
+
total_q = int(qnope.shape[0] + qrope.shape[0])
|
|
278
|
+
h = out["hidden_size"]
|
|
279
|
+
if total_q > 0 and h % total_q == 0:
|
|
280
|
+
# For MiniMind/Chronos checkpoints head_dim is
|
|
281
|
+
# hidden_size / num_attention_heads, so total_q equals
|
|
282
|
+
# hidden_size and n_heads is recovered by divisibility.
|
|
283
|
+
pass
|
|
284
|
+
candidates = [
|
|
285
|
+
n for n in range(1, out["hidden_size"] + 1)
|
|
286
|
+
if out["hidden_size"] % n == 0
|
|
287
|
+
and int(qrope.shape[0]) % n == 0
|
|
288
|
+
and int(qnope.shape[0]) % n == 0
|
|
289
|
+
]
|
|
290
|
+
if candidates:
|
|
291
|
+
# Prefer the configured/default 8 when valid; else
|
|
292
|
+
# choose the largest plausible head count.
|
|
293
|
+
preferred = 8 if 8 in candidates else max(candidates)
|
|
294
|
+
head_dim = out["hidden_size"] // preferred
|
|
295
|
+
rope_dim = int(qrope.shape[0]) // preferred
|
|
296
|
+
nope_dim = int(qnope.shape[0]) // preferred
|
|
297
|
+
if rope_dim + nope_dim == head_dim:
|
|
298
|
+
out["num_attention_heads"] = preferred
|
|
299
|
+
out["rope_dim"] = rope_dim
|
|
300
|
+
out["num_attention_heads_total_dim"] = preferred * head_dim
|
|
301
|
+
if kvdown is not None:
|
|
302
|
+
out["kv_latent_dim"] = int(kvdown.shape[0])
|
|
303
|
+
if vproj is not None and out.get("kv_latent_dim"):
|
|
304
|
+
head_dim = out.get("hidden_size", 0) // max(out.get("num_attention_heads", 8), 1)
|
|
305
|
+
if head_dim > 0:
|
|
306
|
+
out["num_key_value_heads"] = max(1, int(vproj.shape[0]) // head_dim)
|
|
307
|
+
out["num_kv_heads_total_dim"] = int(vproj.shape[0])
|
|
308
|
+
|
|
309
|
+
# Odd layers are SlidingWindowAttention in the hybrid stack.
|
|
310
|
+
# If layer 1 exists, its q/k projection shapes are a direct
|
|
311
|
+
# source of head counts and should override weaker defaults.
|
|
312
|
+
sw_q = sd.get("model.layers.1.self_attn.q_proj.weight")
|
|
313
|
+
sw_k = sd.get("model.layers.1.self_attn.k_proj.weight")
|
|
314
|
+
if sw_q is not None and sw_k is not None and out.get("hidden_size"):
|
|
315
|
+
out["num_attention_heads_total_dim"] = int(sw_q.shape[0])
|
|
316
|
+
out["num_kv_heads_total_dim"] = int(sw_k.shape[0])
|
|
317
|
+
return out
|
|
318
|
+
except Exception:
|
|
319
|
+
return {}
|
|
320
|
+
|
|
321
|
+
def generate(cfg, selected_backend, model_path_val, prompt_val, max_tok, temp):
|
|
322
|
+
from transformers import AutoTokenizer
|
|
323
|
+
from chronos.model.config import ChronosConfig
|
|
324
|
+
|
|
325
|
+
if not prompt_val.strip():
|
|
326
|
+
return "Please enter a prompt.", 0.0
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
# Forward ALL Config-tab fields so the model topology built
|
|
330
|
+
# here matches the topology the user actually configured.
|
|
331
|
+
# Hardcoding only hidden_size/num_layers/num_experts caused
|
|
332
|
+
# checkpoint↔model mismatches like:
|
|
333
|
+
# "matmul (1,1,768) × (256,6400)"
|
|
334
|
+
# when a checkpoint trained at H=768 was loaded into a
|
|
335
|
+
# H=256-default ChronosConfig.
|
|
336
|
+
cfg = cfg or {}
|
|
337
|
+
# ── Auto-derive topology from the checkpoint ──
|
|
338
|
+
# If a model path is given, sniff the saved tensor shapes
|
|
339
|
+
# and override the Config fields with what the checkpoint
|
|
340
|
+
# actually requires. This makes inference "just work"
|
|
341
|
+
# regardless of which preset is loaded in the Config tab.
|
|
342
|
+
sniffed = _sniff_checkpoint(model_path_val) if model_path_val else {}
|
|
343
|
+
sniff_note = ""
|
|
344
|
+
if sniffed:
|
|
345
|
+
summary = ", ".join(
|
|
346
|
+
f"{k}={v}" for k, v in sniffed.items()
|
|
347
|
+
if k in ("vocab_size", "hidden_size", "num_hidden_layers",
|
|
348
|
+
"num_experts", "moe_intermediate_size")
|
|
349
|
+
)
|
|
350
|
+
sniff_note = f"[sniffed from {model_path_val}: {summary}]\n"
|
|
351
|
+
|
|
352
|
+
model_cfg_kwargs = {
|
|
353
|
+
"hidden_size": sniffed.get("hidden_size", cfg.get("hidden_size", 768)),
|
|
354
|
+
"num_hidden_layers": sniffed.get("num_hidden_layers", cfg.get("num_hidden_layers", 8)),
|
|
355
|
+
"num_experts": sniffed.get("num_experts", cfg.get("num_experts", 4)),
|
|
356
|
+
"num_experts_per_tok": cfg.get("num_experts_per_tok", 1),
|
|
357
|
+
"num_shared_experts": cfg.get("num_shared_experts", 1),
|
|
358
|
+
"lookahead_steps": cfg.get("lookahead_steps", 2),
|
|
359
|
+
"kv_latent_dim": sniffed.get("kv_latent_dim", cfg.get("kv_latent_dim", 64)),
|
|
360
|
+
"sliding_window_size": cfg.get("sliding_window_size", 2048),
|
|
361
|
+
"use_hybrid_attention": cfg.get("use_hybrid_attention", True),
|
|
362
|
+
"vram_budget_gb": cfg.get("vram_budget_gb", 4.0),
|
|
363
|
+
"use_moe": True,
|
|
364
|
+
}
|
|
365
|
+
if "rope_dim" in sniffed:
|
|
366
|
+
model_cfg_kwargs["rope_dim"] = sniffed["rope_dim"]
|
|
367
|
+
if "num_attention_heads" in sniffed:
|
|
368
|
+
model_cfg_kwargs["num_attention_heads"] = sniffed["num_attention_heads"]
|
|
369
|
+
if "num_key_value_heads" in sniffed:
|
|
370
|
+
model_cfg_kwargs["num_key_value_heads"] = sniffed["num_key_value_heads"]
|
|
371
|
+
if "vocab_size" in sniffed:
|
|
372
|
+
model_cfg_kwargs["vocab_size"] = sniffed["vocab_size"]
|
|
373
|
+
if "moe_intermediate_size" in sniffed:
|
|
374
|
+
model_cfg_kwargs["intermediate_size"] = sniffed["moe_intermediate_size"]
|
|
375
|
+
model_cfg_kwargs["moe_intermediate_size"] = sniffed["moe_intermediate_size"]
|
|
376
|
+
|
|
377
|
+
for opt_key, cfg_key in [
|
|
378
|
+
("num_attention_heads", "num_attention_heads"),
|
|
379
|
+
("num_key_value_heads", "num_key_value_heads"),
|
|
380
|
+
("rope_dim", "rope_dim"),
|
|
381
|
+
("max_position_embeddings", "max_seq_len"),
|
|
382
|
+
("tie_word_embeddings", "tie_word_embeddings"),
|
|
383
|
+
]:
|
|
384
|
+
if opt_key in {"num_attention_heads", "num_key_value_heads", "rope_dim"} \
|
|
385
|
+
and opt_key in model_cfg_kwargs:
|
|
386
|
+
continue
|
|
387
|
+
if cfg_key not in cfg:
|
|
388
|
+
continue
|
|
389
|
+
val = cfg[cfg_key]
|
|
390
|
+
if val in (None, "", 0):
|
|
391
|
+
continue
|
|
392
|
+
model_cfg_kwargs[opt_key] = val
|
|
393
|
+
|
|
394
|
+
# If checkpoint sniffing revealed attention dims, adjust heads
|
|
395
|
+
# to match. We prefer the sniffed q/kv totals when the derived
|
|
396
|
+
# head_dim would otherwise be inconsistent.
|
|
397
|
+
if "num_attention_heads_total_dim" in sniffed:
|
|
398
|
+
h = model_cfg_kwargs["hidden_size"]
|
|
399
|
+
n_heads = model_cfg_kwargs.get("num_attention_heads", cfg.get("num_attention_heads", 8))
|
|
400
|
+
head_dim = h // n_heads
|
|
401
|
+
if head_dim > 0:
|
|
402
|
+
correct_n_heads = sniffed["num_attention_heads_total_dim"] // head_dim
|
|
403
|
+
correct_n_kv = sniffed["num_kv_heads_total_dim"] // head_dim
|
|
404
|
+
if correct_n_heads > 0:
|
|
405
|
+
model_cfg_kwargs["num_attention_heads"] = correct_n_heads
|
|
406
|
+
if correct_n_kv > 0:
|
|
407
|
+
model_cfg_kwargs["num_key_value_heads"] = correct_n_kv
|
|
408
|
+
|
|
409
|
+
model_cfg = ChronosConfig(**model_cfg_kwargs)
|
|
410
|
+
|
|
411
|
+
backend, backend_note = _resolve_inference_backend(selected_backend, model_path_val, sniffed)
|
|
412
|
+
tokenizer = AutoTokenizer.from_pretrained(chronos.deps.get_tokenizer_path())
|
|
413
|
+
token_ids = tokenizer.encode(prompt_val)
|
|
414
|
+
generated = []
|
|
415
|
+
t0 = time.monotonic()
|
|
416
|
+
|
|
417
|
+
if backend == "mlx":
|
|
418
|
+
import mlx.core as mx
|
|
419
|
+
from chronos.mlx.model import ChronosMLXModel
|
|
420
|
+
from chronos.mlx.inference import ChronosMLXInferenceEngine
|
|
421
|
+
|
|
422
|
+
model = ChronosMLXModel(model_cfg)
|
|
423
|
+
if model_path_val and __import__("os").path.exists(model_path_val):
|
|
424
|
+
import numpy as np, torch
|
|
425
|
+
sd = torch.load(model_path_val, map_location="cpu")
|
|
426
|
+
# best-effort weight copy via numpy bridge
|
|
427
|
+
for k, v in sd.items():
|
|
428
|
+
arr = mx.array(v.float().numpy())
|
|
429
|
+
# walk model attributes by key path
|
|
430
|
+
try:
|
|
431
|
+
parts = k.split(".")
|
|
432
|
+
obj = model
|
|
433
|
+
for p in parts[:-1]:
|
|
434
|
+
obj = getattr(obj, p) if not p.isdigit() else obj[int(p)]
|
|
435
|
+
setattr(obj, parts[-1], arr)
|
|
436
|
+
except Exception:
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
engine = ChronosMLXInferenceEngine(model, model_cfg)
|
|
440
|
+
input_arr = mx.array([token_ids])
|
|
441
|
+
for tok_id in engine.generate(input_arr, max_new_tokens=int(max_tok),
|
|
442
|
+
temperature=float(temp)):
|
|
443
|
+
generated.append(tok_id)
|
|
444
|
+
if tok_id == tokenizer.eos_token_id:
|
|
445
|
+
break
|
|
446
|
+
engine.stop()
|
|
447
|
+
|
|
448
|
+
else:
|
|
449
|
+
generated = _run_torch_inference(
|
|
450
|
+
backend,
|
|
451
|
+
model_cfg,
|
|
452
|
+
model_path_val,
|
|
453
|
+
token_ids,
|
|
454
|
+
int(max_tok),
|
|
455
|
+
float(temp),
|
|
456
|
+
tokenizer.eos_token_id,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
elapsed = time.monotonic() - t0
|
|
460
|
+
tps = len(generated) / max(elapsed, 1e-6)
|
|
461
|
+
decoded = tokenizer.decode(generated, skip_special_tokens=True)
|
|
462
|
+
return f"{backend_note}{sniff_note}[{backend}] {decoded}", round(tps, 1)
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
import traceback
|
|
466
|
+
msg = str(e)
|
|
467
|
+
hint = ""
|
|
468
|
+
# Detect the common "config doesn't match checkpoint" case and
|
|
469
|
+
# inspect the saved weights to tell the user exactly which
|
|
470
|
+
# hidden_size/vocab_size the checkpoint expects.
|
|
471
|
+
if ("matmul" in msg or "shape" in msg.lower() or "size mismatch" in msg.lower()) \
|
|
472
|
+
and model_path_val and __import__("os").path.exists(model_path_val):
|
|
473
|
+
try:
|
|
474
|
+
import torch as _t
|
|
475
|
+
sd = _t.load(model_path_val, map_location="cpu")
|
|
476
|
+
if isinstance(sd, dict):
|
|
477
|
+
embed = sd.get("model.embed_tokens.weight")
|
|
478
|
+
if embed is not None:
|
|
479
|
+
vocab, h = tuple(embed.shape)
|
|
480
|
+
hint = (
|
|
481
|
+
f"\n\nHINT: the checkpoint `{model_path_val}` was "
|
|
482
|
+
f"saved with vocab_size={vocab}, hidden_size={h}. "
|
|
483
|
+
f"Set these in the Config tab (and click Save Config), "
|
|
484
|
+
f"then retry."
|
|
485
|
+
)
|
|
486
|
+
except Exception:
|
|
487
|
+
pass
|
|
488
|
+
return f"Error: {msg}{hint}\n{traceback.format_exc()}", 0.0
|
|
489
|
+
|
|
490
|
+
gen_btn.click(
|
|
491
|
+
fn=generate,
|
|
492
|
+
inputs=[config_state, inference_backend, model_path, prompt, max_tokens, temperature],
|
|
493
|
+
outputs=[output_box, tps_box],
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return tab
|
ui/tabs/iomon_tab.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""ui/tabs/iomon_tab.py — Live IO/cache metrics monitor."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from ui.gradio_compat import gr
|
|
5
|
+
|
|
6
|
+
from ui.i18n import t, register_translatable
|
|
7
|
+
from chronos.runtime.metrics import bus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _poll() -> dict:
|
|
11
|
+
snap = bus.snapshot()
|
|
12
|
+
out = {}
|
|
13
|
+
for k, v in snap.items():
|
|
14
|
+
if not v:
|
|
15
|
+
continue
|
|
16
|
+
out[k] = {"latest": v[-1][1], "n_samples": len(v)}
|
|
17
|
+
return out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_iomon_tab():
|
|
21
|
+
with gr.Tab(t("tab.iomon")) as tab:
|
|
22
|
+
register_translatable(tab, "tab.iomon")
|
|
23
|
+
gr.Markdown(f"### {t('iomon.title')}")
|
|
24
|
+
|
|
25
|
+
metrics_box = gr.JSON(label="metrics snapshot")
|
|
26
|
+
refresh_btn = gr.Button(t("iomon.refresh"))
|
|
27
|
+
register_translatable(refresh_btn, "iomon.refresh")
|
|
28
|
+
|
|
29
|
+
refresh_btn.click(fn=_poll, outputs=[metrics_box])
|
|
30
|
+
|
|
31
|
+
# Auto-refresh every 2s while tab is open
|
|
32
|
+
timer = gr.Timer(2.0)
|
|
33
|
+
timer.tick(fn=_poll, outputs=[metrics_box])
|
|
34
|
+
|
|
35
|
+
# Initial value
|
|
36
|
+
metrics_box.value = _poll()
|
|
37
|
+
return tab
|
ui/tabs/pipeline_tab.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""ui/tabs/pipeline_tab.py — Six-stage training pipeline view.
|
|
2
|
+
|
|
3
|
+
Each stage shells out to its top-level entry script (train_chronos*.py).
|
|
4
|
+
Each stage has its OWN data_path so different stages can use different
|
|
5
|
+
fixtures (pretrain corpus vs SFT conversations vs DPO pairs vs GRPO prompts
|
|
6
|
+
vs distill-from-SFT). Status updates stream back into a status board.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from ui.gradio_compat import gr
|
|
13
|
+
|
|
14
|
+
from chronos.backend import training_available, resolve_training_device
|
|
15
|
+
from ui.i18n import t, register_translatable
|
|
16
|
+
from ui.tabs.train_tab import _distill_teacher_placeholder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _repo_root() -> str:
|
|
20
|
+
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fix(p: str) -> str:
|
|
24
|
+
return os.path.join(_repo_root(), p)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# (display name, script, default --from_weight, default data fixture, takes_teacher_path)
|
|
28
|
+
STAGES = [
|
|
29
|
+
("Pretrain", "train_chronos.py", "", "tests/fixtures/tiny_pretrain.jsonl", False),
|
|
30
|
+
("SFT", "train_chronos_sft.py", "chronos","tests/fixtures/tiny_sft.jsonl", False),
|
|
31
|
+
("DPO", "train_chronos_dpo.py", "sft", "tests/fixtures/tiny_dpo.jsonl", False),
|
|
32
|
+
("ORPO", "train_chronos_orpo.py", "sft", "tests/fixtures/tiny_dpo.jsonl", False),
|
|
33
|
+
("GRPO", "train_chronos_grpo.py", "orpo", "tests/fixtures/tiny_grpo.jsonl", False),
|
|
34
|
+
("Distill", "train_chronos_distill.py", "grpo", "tests/fixtures/tiny_sft.jsonl", True),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
PIPELINE_TRAIN_BACKEND_CHOICES = ["auto"] + [name for name in ("cuda", "xpu", "mps", "cpu") if name in set(training_available())]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_pipeline_tab():
|
|
41
|
+
with gr.Tab(t("tab.pipeline")) as tab:
|
|
42
|
+
register_translatable(tab, "tab.pipeline")
|
|
43
|
+
gr.Markdown(f"### {t('pipeline.title')}")
|
|
44
|
+
|
|
45
|
+
with gr.Row():
|
|
46
|
+
save_dir = gr.Textbox(value="out", label=t("pipeline.save_dir"), scale=2)
|
|
47
|
+
steps = gr.Number(value=30, precision=0, label=t("pipeline.steps"), scale=1)
|
|
48
|
+
train_backend = gr.Dropdown(
|
|
49
|
+
choices=PIPELINE_TRAIN_BACKEND_CHOICES,
|
|
50
|
+
value="auto",
|
|
51
|
+
label=t("train.backend"),
|
|
52
|
+
scale=1,
|
|
53
|
+
)
|
|
54
|
+
register_translatable(save_dir, "pipeline.save_dir")
|
|
55
|
+
register_translatable(steps, "pipeline.steps")
|
|
56
|
+
register_translatable(train_backend, "train.backend")
|
|
57
|
+
|
|
58
|
+
# Per-stage rows: name + data_path + (optional) teacher_path + Run button
|
|
59
|
+
per_stage_rows = [] # list of dicts {data, teacher_or_None, status, run_btn}
|
|
60
|
+
for i, (name, script, from_w, default_data, takes_teacher) in enumerate(STAGES):
|
|
61
|
+
with gr.Row():
|
|
62
|
+
gr.Markdown(f"**{i+1}. {name}**", elem_classes=[])
|
|
63
|
+
# Default to a RELATIVE path so the textbox is portable across
|
|
64
|
+
# checkouts. Subprocess runs with cwd=repo_root, so the path
|
|
65
|
+
# resolves against the project directory.
|
|
66
|
+
data_box = gr.Textbox(value=default_data, label=f"{name} {t('pipeline.data_path')}", scale=3)
|
|
67
|
+
teacher_box = (
|
|
68
|
+
gr.Textbox(value="", label=f"{name} {t('pipeline.teacher_path')}",
|
|
69
|
+
placeholder=_distill_teacher_placeholder(), scale=2)
|
|
70
|
+
if takes_teacher else None
|
|
71
|
+
)
|
|
72
|
+
status_box = gr.Textbox(value="pending", label=t("pipeline.status"), interactive=False, scale=1)
|
|
73
|
+
run_btn = gr.Button(f"▶ {name}", scale=1)
|
|
74
|
+
per_stage_rows.append({
|
|
75
|
+
"name": name, "script": script, "from_weight": from_w,
|
|
76
|
+
"data": data_box, "teacher": teacher_box,
|
|
77
|
+
"status": status_box, "run_btn": run_btn, "takes_teacher": takes_teacher,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
log_box = gr.Textbox(label="log (stage runs append)", lines=20, interactive=False, autoscroll=True)
|
|
81
|
+
clear_log = gr.Button(t("pipeline.clear_log"))
|
|
82
|
+
register_translatable(clear_log, "pipeline.clear_log")
|
|
83
|
+
clear_log.click(fn=lambda: "", outputs=[log_box])
|
|
84
|
+
|
|
85
|
+
def make_runner(idx):
|
|
86
|
+
row = per_stage_rows[idx]
|
|
87
|
+
name = row["name"]; script = row["script"]; from_w = row["from_weight"]
|
|
88
|
+
takes_teacher = row["takes_teacher"]
|
|
89
|
+
|
|
90
|
+
def run(_save_dir, _steps, _backend, _data, _teacher, current_log):
|
|
91
|
+
_, resolved_device = resolve_training_device(_backend)
|
|
92
|
+
cmd = [sys.executable, script, # relative — resolved against cwd
|
|
93
|
+
"--data_path", _data,
|
|
94
|
+
"--save_dir", _save_dir,
|
|
95
|
+
"--steps", str(int(_steps)),
|
|
96
|
+
"--device", resolved_device]
|
|
97
|
+
if from_w:
|
|
98
|
+
cmd += ["--from_weight", from_w]
|
|
99
|
+
if takes_teacher:
|
|
100
|
+
if not _teacher:
|
|
101
|
+
# default to sft checkpoint at the configured save_dir
|
|
102
|
+
_teacher = os.path.join(_save_dir, "sft_192_moe.pth")
|
|
103
|
+
cmd += ["--teacher_path", _teacher]
|
|
104
|
+
|
|
105
|
+
lines = (current_log.split("\n") if current_log else [])
|
|
106
|
+
lines.append(f"\n──── [{name}] $ {' '.join(cmd)} ────")
|
|
107
|
+
yield "running", "\n".join(lines[-500:])
|
|
108
|
+
try:
|
|
109
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
|
110
|
+
stderr=subprocess.STDOUT, text=True, bufsize=1,
|
|
111
|
+
cwd=_repo_root())
|
|
112
|
+
for line in proc.stdout:
|
|
113
|
+
lines.append(f"[{name}] {line.rstrip()}")
|
|
114
|
+
yield "running", "\n".join(lines[-500:])
|
|
115
|
+
proc.wait()
|
|
116
|
+
final = "done" if proc.returncode == 0 else f"failed ({proc.returncode})"
|
|
117
|
+
lines.append(f"──── [{name}] {final} ────")
|
|
118
|
+
yield final, "\n".join(lines[-500:])
|
|
119
|
+
except Exception as e:
|
|
120
|
+
lines.append(f"[{name}] error: {e}")
|
|
121
|
+
yield "failed", "\n".join(lines[-500:])
|
|
122
|
+
return run
|
|
123
|
+
|
|
124
|
+
for i, row in enumerate(per_stage_rows):
|
|
125
|
+
inputs = [save_dir, steps, train_backend, row["data"]]
|
|
126
|
+
inputs.append(row["teacher"] if row["takes_teacher"] else gr.State(""))
|
|
127
|
+
inputs.append(log_box)
|
|
128
|
+
row["run_btn"].click(
|
|
129
|
+
fn=make_runner(i),
|
|
130
|
+
inputs=inputs,
|
|
131
|
+
outputs=[row["status"], log_box],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return tab
|