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
ui/tabs/config_tab.py ADDED
@@ -0,0 +1,318 @@
1
+ """
2
+ ui/tabs/config_tab.py — ChronosConfig editor + live param estimator sidebar.
3
+
4
+ Merges the former Designer tab: the architecture sliders here are the
5
+ single source of truth; the right-hand "Estimator" panel reads the same
6
+ state and displays parameter counts, memory footprints and estimated
7
+ decode tokens/sec in real time.
8
+
9
+ Data-path is intentionally NOT in this tab — it lives in the Train tab
10
+ (and is per-stage in the Pipeline tab) so each workflow owns its own
11
+ dataset choice. Only save_dir is shared model-wide.
12
+ """
13
+ from ui.gradio_compat import gr
14
+
15
+ from ui.i18n import t, register_translatable
16
+ from ui.estimator import (
17
+ ArchConfig, total_params, active_params, memory_footprint,
18
+ estimated_decode_tps, fmt_bytes, fmt_params,
19
+ )
20
+ from ui.presets import (
21
+ preset_names, get_preset, values_in_input_order, save_config, load_config,
22
+ )
23
+
24
+
25
+ # Shared slider ranges (match Designer v1, which had the more permissive
26
+ # bounds). The old Config tab had stricter bounds that surprised users
27
+ # toggling between tabs and rejected values they typed manually.
28
+ RANGES = {
29
+ "hidden_size": (8, 16384, 8),
30
+ "num_hidden_layers": (1, 128, 1),
31
+ "num_experts": (1, 1024, 1),
32
+ "experts_per_tok": (1, 32, 1),
33
+ "shared_experts": (0, 32, 1),
34
+ "lookahead_steps": (0, 16, 1),
35
+ "kv_latent_dim": (4, 1024, 4),
36
+ "rope_dim": (2, 256, 2),
37
+ "sliding_window": (8, 65536, 8),
38
+ "moe_intermediate": (8, 65536, 8),
39
+ "vocab_size": (32, 200000, 32),
40
+ }
41
+
42
+
43
+ def _estimate(cfg_dict: dict):
44
+ """Compute estimator outputs from a config dict.
45
+
46
+ A moe_intermediate_size of 0 (or missing) is treated as "auto-derive
47
+ from hidden_size", matching MiniMindConfig's default rule. This keeps
48
+ the estimator consistent with what the trainer actually builds.
49
+ """
50
+ mis = int(cfg_dict.get("moe_intermediate_size", 0) or 0)
51
+ its = int(cfg_dict.get("intermediate_size", 0) or 0)
52
+ arch = ArchConfig(
53
+ hidden_size=int(cfg_dict.get("hidden_size", 256)),
54
+ num_hidden_layers=int(cfg_dict.get("num_hidden_layers", 4)),
55
+ num_attention_heads=int(cfg_dict.get("num_attention_heads", 8)),
56
+ num_key_value_heads=int(cfg_dict.get("num_key_value_heads", 4)),
57
+ vocab_size=int(cfg_dict.get("vocab_size", 6400)),
58
+ intermediate_size=its if its > 0 else None,
59
+ moe_intermediate_size=mis if mis > 0 else None,
60
+ num_experts=int(cfg_dict.get("num_experts", 4)),
61
+ num_experts_per_tok=int(cfg_dict.get("num_experts_per_tok", 1)),
62
+ num_shared_experts=int(cfg_dict.get("num_shared_experts", 1)),
63
+ lookahead_steps=int(cfg_dict.get("lookahead_steps", 2)),
64
+ kv_latent_dim=int(cfg_dict.get("kv_latent_dim", 64)),
65
+ rope_dim=int(cfg_dict.get("rope_dim", 32)),
66
+ sliding_window_size=int(cfg_dict.get("sliding_window_size", 2048)),
67
+ use_hybrid_attention=bool(cfg_dict.get("use_hybrid_attention", True)),
68
+ tie_word_embeddings=bool(cfg_dict.get("tie_word_embeddings", True)),
69
+ dtype=str(cfg_dict.get("dtype", "fp16")),
70
+ )
71
+ mem = memory_footprint(arch)
72
+ return (
73
+ fmt_params(total_params(arch)),
74
+ fmt_params(active_params(arch)),
75
+ fmt_bytes(mem["vram_estimate_bytes"]),
76
+ fmt_bytes(mem["ssd_estimate_bytes"]),
77
+ fmt_bytes(mem["kv_cache_bytes"]),
78
+ f"{estimated_decode_tps(arch):.1f}",
79
+ )
80
+
81
+
82
+ def build_config_tab():
83
+ """Returns (config_state, list_of_input_components, save_dir).
84
+
85
+ Note: the pre-M6 signature returned (state, inputs, data_path, save_dir).
86
+ data_path has been moved to the Train tab, so the tuple now has 3
87
+ elements. Callers must be updated.
88
+ """
89
+ initial_preset = "Recommended-CN (≈120M)"
90
+ initial_cfg = get_preset(initial_preset)
91
+ config_state = gr.State(dict(initial_cfg))
92
+
93
+ with gr.Tab(t("tab.config")) as tab:
94
+ register_translatable(tab, "tab.config")
95
+
96
+ # ── Presets / Save / Load (top of tab) ─────────────────
97
+ with gr.Row():
98
+ preset_dd = gr.Dropdown(
99
+ choices=preset_names(),
100
+ value=initial_preset,
101
+ label=t("config.preset"),
102
+ scale=2,
103
+ )
104
+ register_translatable(preset_dd, "config.preset")
105
+ apply_preset_btn = gr.Button(t("config.load_preset"), variant="secondary", scale=1)
106
+ register_translatable(apply_preset_btn, "config.load_preset")
107
+ reset_btn = gr.Button(t("config.reset_minimind"), variant="secondary", scale=1)
108
+ register_translatable(reset_btn, "config.reset_minimind")
109
+ with gr.Row():
110
+ cfg_path = gr.Textbox(
111
+ value="./out/chronos_config.json",
112
+ label=t("config.cfg_path"),
113
+ scale=3,
114
+ )
115
+ register_translatable(cfg_path, "config.cfg_path")
116
+ save_btn = gr.Button(t("config.save_cfg"), variant="primary", scale=1)
117
+ register_translatable(save_btn, "config.save_cfg")
118
+ load_btn = gr.Button(t("config.load_cfg"), variant="primary", scale=1)
119
+ register_translatable(load_btn, "config.load_cfg")
120
+ save_status = gr.Markdown("", visible=True)
121
+
122
+ with gr.Row():
123
+ with gr.Column(scale=3):
124
+ gr.Markdown(f"### {t('config.arch')}")
125
+
126
+ D = initial_cfg
127
+
128
+ with gr.Row():
129
+ hidden_size = gr.Slider(*RANGES["hidden_size"], value=D["hidden_size"], label=t("config.hidden_size"))
130
+ num_layers = gr.Slider(*RANGES["num_hidden_layers"], value=D["num_hidden_layers"], label=t("config.num_layers"))
131
+ num_experts = gr.Slider(*RANGES["num_experts"], value=D["num_experts"], label=t("config.num_experts"))
132
+ experts_per_tok = gr.Slider(*RANGES["experts_per_tok"], value=D["num_experts_per_tok"], label=t("config.experts_per_tok"))
133
+ register_translatable(hidden_size, "config.hidden_size")
134
+ register_translatable(num_layers, "config.num_layers")
135
+ register_translatable(num_experts, "config.num_experts")
136
+ register_translatable(experts_per_tok, "config.experts_per_tok")
137
+
138
+ with gr.Row():
139
+ num_shared = gr.Slider(*RANGES["shared_experts"], value=D["num_shared_experts"], label=t("config.shared_experts"))
140
+ lookahead = gr.Slider(*RANGES["lookahead_steps"], value=D["lookahead_steps"], label=t("config.lookahead"))
141
+ kv_latent_dim = gr.Slider(*RANGES["kv_latent_dim"], value=D["kv_latent_dim"], label=t("config.kv_latent"))
142
+ sliding_window = gr.Slider(*RANGES["sliding_window"], value=D["sliding_window_size"], label=t("config.sliding_window"))
143
+ register_translatable(num_shared, "config.shared_experts")
144
+ register_translatable(lookahead, "config.lookahead")
145
+ register_translatable(kv_latent_dim, "config.kv_latent")
146
+ register_translatable(sliding_window, "config.sliding_window")
147
+
148
+ with gr.Row():
149
+ num_heads = gr.Slider(1, 256, value=D["num_attention_heads"], step=1, label=t("config.num_attn_heads"))
150
+ num_kv_heads = gr.Slider(1, 256, value=D["num_key_value_heads"], step=1, label=t("config.num_kv_heads"))
151
+ rope_dim = gr.Slider(*RANGES["rope_dim"], value=D["rope_dim"], label=t("config.rope_dim"))
152
+ moe_intermediate = gr.Slider(
153
+ 0, 65536, value=D["moe_intermediate_size"], step=8,
154
+ label=t("config.moe_inter"),
155
+ )
156
+ register_translatable(num_heads, "config.num_attn_heads")
157
+ register_translatable(num_kv_heads, "config.num_kv_heads")
158
+ register_translatable(rope_dim, "config.rope_dim")
159
+ register_translatable(moe_intermediate, "config.moe_inter")
160
+
161
+ with gr.Row():
162
+ vocab_size = gr.Number(value=D["vocab_size"], precision=0, label=t("config.vocab_size"))
163
+ dtype = gr.Dropdown(
164
+ choices=["fp32", "bf16", "fp16", "int8", "nf4"],
165
+ value=D["dtype"], label=t("config.dtype"),
166
+ )
167
+ tie_word_embeddings = gr.Checkbox(value=D["tie_word_embeddings"], label=t("config.tie_emb"))
168
+ register_translatable(vocab_size, "config.vocab_size")
169
+ register_translatable(dtype, "config.dtype")
170
+ register_translatable(tie_word_embeddings, "config.tie_emb")
171
+
172
+ gr.Markdown(f"### {t('config.loss')}")
173
+ with gr.Row():
174
+ lambda_balance = gr.Number(value=D["lambda_balance"], label=t("config.lambda_balance"), precision=6)
175
+ lambda_temporal = gr.Number(value=D["lambda_temporal"], label=t("config.lambda_temporal"), precision=6)
176
+ lambda_lookahead = gr.Number(value=D["lambda_lookahead"], label=t("config.lambda_la"), precision=6)
177
+ lambda_router_anchor = gr.Number(value=D["lambda_router_anchor"], label=t("config.lambda_anchor"), precision=6)
178
+ register_translatable(lambda_balance, "config.lambda_balance")
179
+ register_translatable(lambda_temporal, "config.lambda_temporal")
180
+ register_translatable(lambda_lookahead, "config.lambda_la")
181
+ register_translatable(lambda_router_anchor, "config.lambda_anchor")
182
+
183
+ gr.Markdown(f"### {t('config.hw')}")
184
+ with gr.Row():
185
+ vram_budget = gr.Slider(0.01, 256.0, value=D["vram_budget_gb"], step=0.01, label=t("config.vram_budget"))
186
+ pinned_frac = gr.Slider(0.01, 0.95, value=D["pinned_memory_max_fraction"], step=0.01, label=t("config.pinned_frac"))
187
+ use_hybrid_attn = gr.Checkbox(value=D["use_hybrid_attention"], label=t("config.hybrid_attn"))
188
+ storage_format = gr.Dropdown(choices=["safetensors", "pt"], value=D["storage_format"],
189
+ label=t("config.storage_format"))
190
+ register_translatable(vram_budget, "config.vram_budget")
191
+ register_translatable(pinned_frac, "config.pinned_frac")
192
+ register_translatable(use_hybrid_attn, "config.hybrid_attn")
193
+ register_translatable(storage_format, "config.storage_format")
194
+
195
+ gr.Markdown(f"### {t('config.train')}")
196
+ with gr.Row():
197
+ learning_rate = gr.Number(value=D["learning_rate"], label=t("config.lr"), precision=6)
198
+ batch_size = gr.Slider(1, 512, value=D["batch_size"], step=1, label=t("config.batch_size"))
199
+ accum_steps = gr.Slider(1, 256, value=D["accumulation_steps"], step=1, label=t("config.accum"))
200
+ max_seq_len = gr.Slider(8, 131072, value=D["max_seq_len"], step=8, label=t("config.max_seq_len"))
201
+ register_translatable(learning_rate, "config.lr")
202
+ register_translatable(batch_size, "config.batch_size")
203
+ register_translatable(accum_steps, "config.accum")
204
+ register_translatable(max_seq_len, "config.max_seq_len")
205
+
206
+ with gr.Row():
207
+ epochs = gr.Slider(1, 1000, value=D["epochs"], step=1, label=t("config.epochs"))
208
+ save_interval = gr.Slider(10, 100000, value=D["save_interval"], step=10, label=t("config.save_interval"))
209
+ save_dir = gr.Textbox(label=t("config.save_dir"), value=D["save_dir"])
210
+ register_translatable(epochs, "config.epochs")
211
+ register_translatable(save_interval, "config.save_interval")
212
+ register_translatable(save_dir, "config.save_dir")
213
+
214
+ config_display = gr.JSON(label="Current Config (saved to config_state)", value=initial_cfg)
215
+
216
+ with gr.Column(scale=1, min_width=260):
217
+ gr.Markdown(f"### 🧬 {t('designer.title')}")
218
+ total_box = gr.Textbox(label=t("designer.total"), interactive=False)
219
+ active_box = gr.Textbox(label=t("designer.active"), interactive=False)
220
+ vram_box = gr.Textbox(label=t("designer.vram"), interactive=False)
221
+ ssd_box = gr.Textbox(label=t("designer.ssd"), interactive=False)
222
+ kv_box = gr.Textbox(label=t("designer.kv"), interactive=False)
223
+ tps_box = gr.Textbox(label=t("designer.tps"), interactive=False)
224
+ register_translatable(total_box, "designer.total")
225
+ register_translatable(active_box, "designer.active")
226
+ register_translatable(vram_box, "designer.vram")
227
+ register_translatable(ssd_box, "designer.ssd")
228
+ register_translatable(kv_box, "designer.kv")
229
+ register_translatable(tps_box, "designer.tps")
230
+
231
+ # IMPORTANT: this list also defines the order Autotune uses to push
232
+ # best-params back via gr.update(). Don't reorder without updating
233
+ # ui/tabs/autotune_tab.py.
234
+ all_inputs = [
235
+ hidden_size, num_layers, num_experts, experts_per_tok,
236
+ num_shared, lookahead, kv_latent_dim, sliding_window,
237
+ num_heads, num_kv_heads, rope_dim, moe_intermediate,
238
+ vocab_size, dtype, tie_word_embeddings,
239
+ lambda_balance, lambda_temporal, lambda_lookahead, lambda_router_anchor,
240
+ vram_budget, pinned_frac, use_hybrid_attn, storage_format,
241
+ learning_rate, batch_size, accum_steps, max_seq_len,
242
+ epochs, save_interval, save_dir,
243
+ ]
244
+ estimator_outs = [total_box, active_box, vram_box, ssd_box, kv_box, tps_box]
245
+
246
+ def update_config(*vals):
247
+ (hs, nl, ne, ept, ns, la, kv, sw,
248
+ nh, nkv, rd, moe_i,
249
+ vs, dt, tie,
250
+ lb, lt, ll, lra,
251
+ vb, pf, ha, sf,
252
+ lr, bs, ac, msl, ep, si, sd) = vals
253
+ cfg = {
254
+ "hidden_size": int(hs), "num_hidden_layers": int(nl),
255
+ "num_experts": int(ne), "num_experts_per_tok": int(ept),
256
+ "num_shared_experts": int(ns), "lookahead_steps": int(la),
257
+ "kv_latent_dim": int(kv), "sliding_window_size": int(sw),
258
+ "num_attention_heads": int(nh), "num_key_value_heads": int(nkv),
259
+ "rope_dim": int(rd), "moe_intermediate_size": int(moe_i),
260
+ "vocab_size": int(vs), "dtype": str(dt),
261
+ "tie_word_embeddings": bool(tie),
262
+ "lambda_balance": float(lb), "lambda_temporal": float(lt),
263
+ "lambda_lookahead": float(ll), "lambda_router_anchor": float(lra),
264
+ "vram_budget_gb": float(vb), "pinned_memory_max_fraction": float(pf),
265
+ "use_hybrid_attention": bool(ha),
266
+ "storage_format": str(sf),
267
+ "learning_rate": float(lr), "batch_size": int(bs),
268
+ "accumulation_steps": int(ac), "max_seq_len": int(msl),
269
+ "epochs": int(ep), "save_interval": int(si),
270
+ "save_dir": sd,
271
+ }
272
+ est = _estimate(cfg)
273
+ return (cfg, cfg) + est
274
+
275
+ for inp in all_inputs:
276
+ inp.change(
277
+ fn=update_config,
278
+ inputs=all_inputs,
279
+ outputs=[config_state, config_display] + estimator_outs,
280
+ )
281
+
282
+ # ── Preset / Save / Load wiring ───────────────────────
283
+ def apply_preset(name):
284
+ cfg = get_preset(name)
285
+ return [dict(cfg), dict(cfg)] + [gr.update(value=v) for v in values_in_input_order(cfg)]
286
+
287
+ def reset_minimind():
288
+ cfg = get_preset("MiniMind-MoE (default)")
289
+ return [dict(cfg), dict(cfg)] + [gr.update(value=v) for v in values_in_input_order(cfg)]
290
+
291
+ def do_save(cfg, path):
292
+ try:
293
+ abs_path = save_config(cfg or {}, path)
294
+ return f"✅ Saved to `{abs_path}`"
295
+ except Exception as e:
296
+ return f"❌ Save failed: {e}"
297
+
298
+ def do_load(path):
299
+ try:
300
+ cfg = load_config(path)
301
+ vals = values_in_input_order(cfg)
302
+ return [f"✅ Loaded from `{path}`", dict(cfg), dict(cfg)] + [gr.update(value=v) for v in vals]
303
+ except FileNotFoundError:
304
+ return [f"❌ Not found: `{path}`", gr.update(), gr.update()] + [gr.update() for _ in all_inputs]
305
+ except Exception as e:
306
+ return [f"❌ Load failed: {e}", gr.update(), gr.update()] + [gr.update() for _ in all_inputs]
307
+
308
+ apply_preset_btn.click(fn=apply_preset, inputs=[preset_dd], outputs=[config_state, config_display] + all_inputs)
309
+ # Selecting a preset in the dropdown should sync immediately;
310
+ # the explicit "Load Preset" button stays as a re-apply affordance.
311
+ preset_dd.change(fn=apply_preset, inputs=[preset_dd], outputs=[config_state, config_display] + all_inputs)
312
+ reset_btn.click(fn=reset_minimind, outputs=[config_state, config_display] + all_inputs)
313
+ save_btn.click(fn=do_save, inputs=[config_state, cfg_path], outputs=[save_status])
314
+ load_btn.click(fn=do_load, inputs=[cfg_path], outputs=[save_status, config_state, config_display] + all_inputs)
315
+
316
+ total_box.value, active_box.value, vram_box.value, ssd_box.value, kv_box.value, tps_box.value = _estimate(initial_cfg)
317
+
318
+ return config_state, all_inputs, save_dir
ui/tabs/data_tab.py ADDED
@@ -0,0 +1,148 @@
1
+ """
2
+ ui/tabs/data_tab.py — Dataset inspector.
3
+
4
+ Streams a JSONL file (no full-load) and reports:
5
+ - record count
6
+ - char-length distribution (min / mean / median / p95 / max)
7
+ - tokenized-length distribution if a tokenizer is available
8
+ - unique-record ratio (cheap dedup heuristic via line hash)
9
+ - first ~3 sample previews
10
+ - matplotlib histogram of token lengths
11
+
12
+ The point: before training, the user can verify the corpus is what they
13
+ expect — right field name, sane lengths, low duplication. Catches
14
+ "wrong tokenizer" / "all records empty" / "huge dup ratio" before they
15
+ burn 10 hours of training.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import statistics
23
+
24
+ from ui.gradio_compat import gr
25
+
26
+ import chronos.deps # noqa: F401
27
+ from ui.i18n import t, register_translatable
28
+
29
+
30
+ def _extract_text(rec: dict) -> str:
31
+ for k in ("text", "content", "story", "passage", "document", "article"):
32
+ if k in rec and rec[k]:
33
+ return str(rec[k])
34
+ if "conversations" in rec and isinstance(rec["conversations"], list):
35
+ return " ".join(str(c.get("content", c.get("value", ""))) for c in rec["conversations"])
36
+ if "messages" in rec and isinstance(rec["messages"], list):
37
+ return " ".join(str(m.get("content", "")) for m in rec["messages"])
38
+ return " ".join(str(v) for v in rec.values()
39
+ if isinstance(v, (str, int, float)))
40
+
41
+
42
+ def _scan(path: str, max_records: int = 50_000):
43
+ """Stream up to `max_records` lines; return summary dict + length list."""
44
+ if not path or not os.path.exists(path):
45
+ return {"error": f"file not found: {path!r}"}, [], []
46
+ char_lens = []
47
+ samples = []
48
+ seen_hashes = set()
49
+ dup = 0
50
+ n_total = 0
51
+ with open(path, "rb") as f:
52
+ for i, line in enumerate(f):
53
+ if i >= max_records:
54
+ break
55
+ line = line.strip()
56
+ if not line:
57
+ continue
58
+ n_total += 1
59
+ try:
60
+ rec = json.loads(line)
61
+ except json.JSONDecodeError:
62
+ continue
63
+ text = _extract_text(rec)
64
+ char_lens.append(len(text))
65
+ h = hashlib.md5(line).hexdigest()
66
+ if h in seen_hashes:
67
+ dup += 1
68
+ else:
69
+ seen_hashes.add(h)
70
+ if len(samples) < 3:
71
+ samples.append(text[:200])
72
+ if not char_lens:
73
+ return {"error": "no parseable records"}, [], samples
74
+
75
+ summary = {
76
+ "scanned": n_total,
77
+ "parseable": len(char_lens),
78
+ "dup_ratio": round(dup / max(n_total, 1), 4),
79
+ "char_min": min(char_lens),
80
+ "char_mean": round(statistics.mean(char_lens), 1),
81
+ "char_median": int(statistics.median(char_lens)),
82
+ "char_p95": int(sorted(char_lens)[int(0.95 * len(char_lens))]),
83
+ "char_max": max(char_lens),
84
+ "file_size_mb": round(os.path.getsize(path) / (1024 * 1024), 1),
85
+ "note": (f"sampled first {max_records} records"
86
+ if n_total >= max_records else "full scan"),
87
+ }
88
+ return summary, char_lens, samples
89
+
90
+
91
+ def _hist(lens):
92
+ if not lens:
93
+ return None
94
+ try:
95
+ import matplotlib
96
+ matplotlib.use("Agg")
97
+ import matplotlib.pyplot as plt
98
+ except ModuleNotFoundError:
99
+ return None
100
+
101
+ plt.close("all")
102
+ fig, ax = plt.subplots(figsize=(7, 3))
103
+ fig.patch.set_facecolor("#1a1a2e")
104
+ ax.set_facecolor("#16213e")
105
+ ax.hist(lens, bins=40, color="#39d98a", edgecolor="#1a1a2e")
106
+ ax.set_xlabel("chars per record", color="#aaa")
107
+ ax.set_ylabel("frequency", color="#aaa")
108
+ ax.tick_params(colors="#aaa")
109
+ for s in ax.spines.values():
110
+ s.set_edgecolor("#333")
111
+ return fig
112
+
113
+
114
+ def build_data_tab():
115
+ with gr.Tab(t("tab.data")) as tab:
116
+ register_translatable(tab, "tab.data")
117
+ gr.Markdown(f"### {t('data.title')}")
118
+
119
+ with gr.Row():
120
+ data_path = gr.Textbox(
121
+ label=t("data.path"), scale=4,
122
+ placeholder="/Users/Fona/Downloads/Hybrid_LLM/Dataset/pretrain_t2t_mini.jsonl",
123
+ )
124
+ scan_btn = gr.Button(t("data.scan"), variant="primary", scale=1)
125
+ register_translatable(data_path, "data.path")
126
+ register_translatable(scan_btn, "data.scan")
127
+
128
+ summary_box = gr.JSON(label=t("data.summary"), value={})
129
+ register_translatable(summary_box, "data.summary")
130
+
131
+ preview_box = gr.Textbox(
132
+ label=t("data.preview"), lines=6, interactive=False, value="",
133
+ )
134
+ register_translatable(preview_box, "data.preview")
135
+
136
+ hist_plot = gr.Plot(label=t("data.length_hist"))
137
+ register_translatable(hist_plot, "data.length_hist")
138
+
139
+ def do_scan(path):
140
+ summary, lens, samples = _scan(path)
141
+ preview = "\n---\n".join(samples) if samples else ""
142
+ fig = _hist(lens)
143
+ return summary, preview, fig
144
+
145
+ scan_btn.click(fn=do_scan, inputs=[data_path],
146
+ outputs=[summary_box, preview_box, hist_plot])
147
+
148
+ return tab