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,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
@@ -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