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/train_tab.py
ADDED
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ui/tabs/train_tab.py — Full training loop: Pretrain / SFT / DPO / ORPO / GRPO / Distill
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import glob
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
import queue
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
|
|
12
|
+
from ui.gradio_compat import gr
|
|
13
|
+
|
|
14
|
+
import chronos.deps # auto-bootstrap minimind on sys.path
|
|
15
|
+
from ui.i18n import t, register_translatable, get_current_lang
|
|
16
|
+
from chronos.backend import training_available, resolve_training_device
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
STAGE_UI_ORDER = ["pretrain", "sft", "dpo", "orpo", "grpo", "distill"]
|
|
20
|
+
STAGE_LABELS = {
|
|
21
|
+
"pretrain": "Stage 1 · Pretrain",
|
|
22
|
+
"sft": "Stage 2 · SFT",
|
|
23
|
+
"dpo": "Stage 3 · DPO",
|
|
24
|
+
"orpo": "Stage 4 · ORPO",
|
|
25
|
+
"grpo": "Stage 5 · GRPO",
|
|
26
|
+
"distill": "Stage 6 · Distill",
|
|
27
|
+
}
|
|
28
|
+
STAGE_CHECKPOINT_PREFIX = {
|
|
29
|
+
"pretrain": "chronos",
|
|
30
|
+
"sft": "sft",
|
|
31
|
+
"dpo": "dpo",
|
|
32
|
+
"orpo": "orpo",
|
|
33
|
+
"grpo": "grpo",
|
|
34
|
+
"distill": "distill",
|
|
35
|
+
}
|
|
36
|
+
STAGE_DEFAULT_INIT = {
|
|
37
|
+
"sft": "chronos",
|
|
38
|
+
"dpo": "sft",
|
|
39
|
+
"orpo": "sft",
|
|
40
|
+
"grpo": "orpo",
|
|
41
|
+
"distill": "grpo",
|
|
42
|
+
}
|
|
43
|
+
STAGE_DEFAULT_DATA = {
|
|
44
|
+
"pretrain": "./tests/fixtures/tiny_pretrain.jsonl",
|
|
45
|
+
"sft": "./tests/fixtures/tiny_sft.jsonl",
|
|
46
|
+
"dpo": "./tests/fixtures/tiny_dpo.jsonl",
|
|
47
|
+
"orpo": "./tests/fixtures/tiny_dpo.jsonl",
|
|
48
|
+
"grpo": "./tests/fixtures/tiny_grpo.jsonl",
|
|
49
|
+
"distill": "./tests/fixtures/tiny_sft.jsonl",
|
|
50
|
+
}
|
|
51
|
+
CHECKPOINT_TOPOLOGY_KEYS = (
|
|
52
|
+
"hidden_size",
|
|
53
|
+
"num_hidden_layers",
|
|
54
|
+
"num_experts",
|
|
55
|
+
"moe_intermediate_size",
|
|
56
|
+
"vocab_size",
|
|
57
|
+
"lookahead_steps",
|
|
58
|
+
"kv_latent_dim",
|
|
59
|
+
"rope_dim",
|
|
60
|
+
"num_attention_heads",
|
|
61
|
+
"num_key_value_heads",
|
|
62
|
+
)
|
|
63
|
+
STAGE_HELP_TEXT = {
|
|
64
|
+
"pretrain": {
|
|
65
|
+
"zh-Hans": "从通用语料继续预训练。`init_weight` 可留空;若存在同名 checkpoint,将按当前拓扑尝试恢复。",
|
|
66
|
+
"zh-Hant": "從通用語料繼續預訓練。`init_weight` 可留空;若存在同名 checkpoint,將按目前拓撲嘗試恢復。",
|
|
67
|
+
"en": "Continue base pretraining on LM-style data. `init_weight` may be left blank; if a matching checkpoint exists, Train will resume from it.",
|
|
68
|
+
"ja": "汎用コーパスで事前学習を続けます。`init_weight` は空で構いません。同名 checkpoint があり拓撲が一致すれば再開します。",
|
|
69
|
+
},
|
|
70
|
+
"sft": {
|
|
71
|
+
"zh-Hans": "使用 `conversations` 数据做监督微调。`init_weight` 留空时,会从当前 `save_dir` 自动接续 `chronos_<H>_moe.pth`;只有手动填路径时才覆盖。",
|
|
72
|
+
"zh-Hant": "使用 `conversations` 資料做監督微調。`init_weight` 留空時,會從目前 `save_dir` 自動接續 `chronos_<H>_moe.pth`;只有手動填路徑時才覆蓋。",
|
|
73
|
+
"en": "Run supervised fine-tuning on `conversations` data. If `init_weight` is blank, Train auto-resolves `chronos_<H>_moe.pth` from the current `save_dir`; only a pasted path overrides it.",
|
|
74
|
+
"ja": "`conversations` データで SFT を行います。`init_weight` を空欄にすると、現在の `save_dir` から `chronos_<H>_moe.pth` を自動解決します。パスを入力した場合のみ上書きします。",
|
|
75
|
+
},
|
|
76
|
+
"dpo": {
|
|
77
|
+
"zh-Hans": "使用 `chosen / rejected` 偏好对做 DPO。`init_weight` 留空时,会自动加载当前 `save_dir` 下的 `sft_<H>_moe.pth`;手动填写路径可覆盖。",
|
|
78
|
+
"zh-Hant": "使用 `chosen / rejected` 偏好對做 DPO。`init_weight` 留空時,會自動載入目前 `save_dir` 下的 `sft_<H>_moe.pth`;手動填寫路徑可覆蓋。",
|
|
79
|
+
"en": "Run DPO on `chosen / rejected` preference pairs. Leaving `init_weight` blank auto-loads `sft_<H>_moe.pth` from the current `save_dir`; paste a path only when you want to override it.",
|
|
80
|
+
"ja": "`chosen / rejected` の選好ペアで DPO を実行します。`init_weight` を空欄にすると、現在の `save_dir` から `sft_<H>_moe.pth` を自動読み込みします。上書きしたい場合だけパスを入力してください。",
|
|
81
|
+
},
|
|
82
|
+
"orpo": {
|
|
83
|
+
"zh-Hans": "使用同样的偏好对做 ORPO。`init_weight` 留空时,会自动加载当前 `save_dir` 下的 `sft_<H>_moe.pth`;手动填写路径可覆盖。",
|
|
84
|
+
"zh-Hant": "使用相同的偏好對做 ORPO。`init_weight` 留空時,會自動載入目前 `save_dir` 下的 `sft_<H>_moe.pth`;手動填寫路徑可覆蓋。",
|
|
85
|
+
"en": "Run ORPO on the same preference-pair format. Leaving `init_weight` blank auto-loads `sft_<H>_moe.pth` from the current `save_dir`; paste a path only when you want to override it.",
|
|
86
|
+
"ja": "同じ選好ペア形式で ORPO を実行します。`init_weight` を空欄にすると、現在の `save_dir` から `sft_<H>_moe.pth` を自動読み込みします。上書きしたい場合だけパスを入力してください。",
|
|
87
|
+
},
|
|
88
|
+
"grpo": {
|
|
89
|
+
"zh-Hans": "使用 prompt 数据做 GRPO rollout。`init_weight` 留空时,会自动加载当前 `save_dir` 下的 `orpo_<H>_moe.pth`;`reward` 可配置。",
|
|
90
|
+
"zh-Hant": "使用 prompt 資料做 GRPO rollout。`init_weight` 留空時,會自動載入目前 `save_dir` 下的 `orpo_<H>_moe.pth`;`reward` 可設定。",
|
|
91
|
+
"en": "Run GRPO rollouts on prompt-style data. Leaving `init_weight` blank auto-loads `orpo_<H>_moe.pth` from the current `save_dir`; `reward` remains configurable.",
|
|
92
|
+
"ja": "prompt 形式のデータで GRPO rollout を行います。`init_weight` を空欄にすると、現在の `save_dir` から `orpo_<H>_moe.pth` を自動読み込みします。`reward` は設定できます。",
|
|
93
|
+
},
|
|
94
|
+
"distill": {
|
|
95
|
+
"zh-Hans": "蒸馏阶段中,学生 `init_weight` 留空时会自动接续当前 `save_dir` 下的 `grpo_<H>_moe.pth`;`teacher_path` 留空时会自动解析 `sft_<H>_moe.pth`。只有手动填路径时才覆盖。",
|
|
96
|
+
"zh-Hant": "蒸餾階段中,學生 `init_weight` 留空時會自動接續目前 `save_dir` 下的 `grpo_<H>_moe.pth`;`teacher_path` 留空時會自動解析 `sft_<H>_moe.pth`。只有手動填路徑時才覆蓋。",
|
|
97
|
+
"en": "For distillation, a blank student `init_weight` auto-resolves `grpo_<H>_moe.pth` from the current `save_dir`, and a blank `teacher_path` auto-resolves `sft_<H>_moe.pth`. Paste paths only when you want to override either one.",
|
|
98
|
+
"ja": "蒸留では、学生側の `init_weight` を空欄にすると現在の `save_dir` から `grpo_<H>_moe.pth` を自動解決し、`teacher_path` を空欄にすると `sft_<H>_moe.pth` を自動解決します。上書きしたい場合だけパスを入力してください。",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
STAGE_METRIC_LABELS = {
|
|
102
|
+
"pretrain": ("Total Loss", "CE Loss", "Aux Loss"),
|
|
103
|
+
"sft": ("SFT Loss", "CE Loss", "Lookahead"),
|
|
104
|
+
"dpo": ("DPO Loss", "Preference", "Anchor"),
|
|
105
|
+
"orpo": ("ORPO Loss", "Chosen NLL", "OR Term"),
|
|
106
|
+
"grpo": ("GRPO Loss", "Policy", "KL"),
|
|
107
|
+
"distill": ("Distill Loss", "KD Loss", "CE Loss"),
|
|
108
|
+
}
|
|
109
|
+
STAGE_CHART_META = {
|
|
110
|
+
"pretrain": {
|
|
111
|
+
"title": "Pretrain Metrics",
|
|
112
|
+
"primary": "CE",
|
|
113
|
+
"secondary": "Aux",
|
|
114
|
+
"tertiary": "Temporal",
|
|
115
|
+
"val": "Validation",
|
|
116
|
+
"speed_title": "Training Speed (steps/s)",
|
|
117
|
+
"speed_label": "steps/s",
|
|
118
|
+
},
|
|
119
|
+
"sft": {
|
|
120
|
+
"title": "SFT Metrics",
|
|
121
|
+
"primary": "CE",
|
|
122
|
+
"secondary": "Lookahead",
|
|
123
|
+
"tertiary": "Anchor",
|
|
124
|
+
"val": "Validation",
|
|
125
|
+
"speed_title": "SFT Speed (steps/s)",
|
|
126
|
+
"speed_label": "steps/s",
|
|
127
|
+
},
|
|
128
|
+
"dpo": {
|
|
129
|
+
"title": "DPO Metrics",
|
|
130
|
+
"primary": "Preference",
|
|
131
|
+
"secondary": "Anchor",
|
|
132
|
+
"tertiary": "Lookahead",
|
|
133
|
+
"val": "Validation",
|
|
134
|
+
"speed_title": "Preference Speed (steps/s)",
|
|
135
|
+
"speed_label": "steps/s",
|
|
136
|
+
},
|
|
137
|
+
"orpo": {
|
|
138
|
+
"title": "ORPO Metrics",
|
|
139
|
+
"primary": "Chosen NLL",
|
|
140
|
+
"secondary": "OR Term",
|
|
141
|
+
"tertiary": "Anchor",
|
|
142
|
+
"val": "Validation",
|
|
143
|
+
"speed_title": "ORPO Speed (steps/s)",
|
|
144
|
+
"speed_label": "steps/s",
|
|
145
|
+
},
|
|
146
|
+
"grpo": {
|
|
147
|
+
"title": "GRPO Metrics",
|
|
148
|
+
"primary": "Policy",
|
|
149
|
+
"secondary": "KL",
|
|
150
|
+
"tertiary": "Reward",
|
|
151
|
+
"val": "Validation",
|
|
152
|
+
"speed_title": "Rollout Speed + Reward",
|
|
153
|
+
"speed_label": "rollouts/s",
|
|
154
|
+
"speed_overlay": "Mean Reward",
|
|
155
|
+
},
|
|
156
|
+
"distill": {
|
|
157
|
+
"title": "Distill Metrics",
|
|
158
|
+
"primary": "KD",
|
|
159
|
+
"secondary": "CE",
|
|
160
|
+
"tertiary": "Anchor",
|
|
161
|
+
"val": "Validation",
|
|
162
|
+
"speed_title": "Distill Speed (steps/s)",
|
|
163
|
+
"speed_label": "steps/s",
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
DEFAULT_STAGE_SAMPLE_PROMPTS = {
|
|
169
|
+
"pretrain": {
|
|
170
|
+
"zh-Hans": "请用简洁中文介绍 Project Chronos 如何把 MoE 的专家加载提前到 Prefill 阶段。",
|
|
171
|
+
"zh-Hant": "請用精簡中文介紹 Project Chronos 如何把 MoE 的專家載入提前到 Prefill 階段。",
|
|
172
|
+
"en": "Explain how Project Chronos moves MoE expert loading into the prefill stage.",
|
|
173
|
+
"ja": "Project Chronos が MoE の専門家ロードを Prefill 段階へ前倒しする仕組みを説明してください。",
|
|
174
|
+
},
|
|
175
|
+
"sft": {
|
|
176
|
+
"zh-Hans": "用户:为什么 Chronos 的双层路由比传统被动 offload 更适合消费级硬件?\n助手:",
|
|
177
|
+
"zh-Hant": "使用者:為什麼 Chronos 的雙層路由比傳統被動 offload 更適合消費級硬體?\n助手:",
|
|
178
|
+
"en": "User: Why is Chronos's dual-layer routing better suited to consumer hardware than reactive expert offload?\nAssistant:",
|
|
179
|
+
"ja": "ユーザー: なぜ Chronos の二層ルーティングは、従来の受動的な expert offload よりも民生向けハードウェアに向いているのですか?\nアシスタント:",
|
|
180
|
+
},
|
|
181
|
+
"dpo": {
|
|
182
|
+
"zh-Hans": "用户:请比较 Chronos 和传统 reactive offload 在推理时的关键差异。\n助手:",
|
|
183
|
+
"zh-Hant": "使用者:請比較 Chronos 與傳統 reactive offload 在推理時的關鍵差異。\n助手:",
|
|
184
|
+
"en": "User: Compare Chronos with conventional reactive offload during inference.\nAssistant:",
|
|
185
|
+
"ja": "ユーザー: 推論時における Chronos と従来の reactive offload の主要な違いを比較してください。\nアシスタント:",
|
|
186
|
+
},
|
|
187
|
+
"orpo": {
|
|
188
|
+
"zh-Hans": "用户:请写一段面向开发者的 Project Chronos 介绍。\n助手:",
|
|
189
|
+
"zh-Hant": "使用者:請寫一段面向開發者的 Project Chronos 介紹。\n助手:",
|
|
190
|
+
"en": "User: Write a developer-facing introduction to Project Chronos.\nAssistant:",
|
|
191
|
+
"ja": "ユーザー: 開発者向けの Project Chronos 紹介文を書いてください。\nアシスタント:",
|
|
192
|
+
},
|
|
193
|
+
"grpo": {
|
|
194
|
+
"zh-Hans": "请比较 Chronos 与传统 MoE offload 在吞吐、首 token 延迟和缓存未命中行为上的权衡。",
|
|
195
|
+
"zh-Hant": "請比較 Chronos 與傳統 MoE offload 在吞吐、首 token 延遲和快取未命中行為上的權衡。",
|
|
196
|
+
"en": "Compare Chronos with conventional MoE offload in terms of throughput, first-token latency, and cache-miss behavior.",
|
|
197
|
+
"ja": "Chronos と従来の MoE offload を、スループット、初回トークン遅延、キャッシュミス時の挙動で比較してください。",
|
|
198
|
+
},
|
|
199
|
+
"distill": {
|
|
200
|
+
"zh-Hans": "请用更现代、更有产品感的英文改写 Project Chronos 的首页介绍,保留核心技术点。",
|
|
201
|
+
"zh-Hant": "請用更現代、更有產品感的英文改寫 Project Chronos 的首頁介紹,保留核心技術點。",
|
|
202
|
+
"en": "Rewrite the Project Chronos landing-page intro in a more modern product voice while preserving the core technical claims.",
|
|
203
|
+
"ja": "Project Chronos のトップ紹介文を、技術的な要点を保ったまま、よりモダンでプロダクト寄りの英語に書き換えてください。",
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _stage_sample_prompt(mode: str, lang: str | None = None) -> str:
|
|
209
|
+
lang = lang or get_current_lang()
|
|
210
|
+
prompts = DEFAULT_STAGE_SAMPLE_PROMPTS.get(mode, DEFAULT_STAGE_SAMPLE_PROMPTS["pretrain"])
|
|
211
|
+
return prompts.get(lang, prompts["en"])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _default_sample_prompts() -> set[str]:
|
|
215
|
+
vals: set[str] = set()
|
|
216
|
+
for prompts in DEFAULT_STAGE_SAMPLE_PROMPTS.values():
|
|
217
|
+
vals.update(v.strip() for v in prompts.values() if v and v.strip())
|
|
218
|
+
return vals
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
DEFAULT_SAMPLE_PROMPT_VALUES = _default_sample_prompts()
|
|
222
|
+
DEFAULT_STAGE_DATA_VALUES = {v.strip() for v in STAGE_DEFAULT_DATA.values() if v.strip()}
|
|
223
|
+
DEFAULT_STAGE_INIT_VALUES = {
|
|
224
|
+
f"./out/{prefix}_768_moe.pth"
|
|
225
|
+
for prefix in STAGE_DEFAULT_INIT.values()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
PRETRAIN_INIT_PLACEHOLDER = {
|
|
229
|
+
"zh-Hans": "留空则按当前 save_dir 自动续训同阶段 checkpoint",
|
|
230
|
+
"zh-Hant": "留空則按目前 save_dir 自動續訓同階段 checkpoint",
|
|
231
|
+
"en": "Leave blank to auto-resume the current-stage checkpoint from save_dir",
|
|
232
|
+
"ja": "空欄なら save_dir から同一ステージ checkpoint を自動再開",
|
|
233
|
+
}
|
|
234
|
+
UPSTREAM_INIT_PLACEHOLDER = {
|
|
235
|
+
"zh-Hans": "留空则按当前 save_dir 自动载入 {prefix}_<H>_moe.pth,手动填写才覆盖",
|
|
236
|
+
"zh-Hant": "留空則按目前 save_dir 自動載入 {prefix}_<H>_moe.pth,手動填寫才覆蓋",
|
|
237
|
+
"en": "Leave blank to auto-load {prefix}_<H>_moe.pth from save_dir; paste a path to override",
|
|
238
|
+
"ja": "空欄なら save_dir から {prefix}_<H>_moe.pth を自動読み込み。上書き時だけパスを指定",
|
|
239
|
+
}
|
|
240
|
+
DISTILL_TEACHER_PLACEHOLDER = {
|
|
241
|
+
"zh-Hans": "留空则按当前 save_dir 自动载入 sft_<H>_moe.pth,手动填写才覆盖",
|
|
242
|
+
"zh-Hant": "留空則按目前 save_dir 自動載入 sft_<H>_moe.pth,手動填寫才覆蓋",
|
|
243
|
+
"en": "Leave blank to auto-load sft_<H>_moe.pth from save_dir; paste a path to override",
|
|
244
|
+
"ja": "空欄なら save_dir から sft_<H>_moe.pth を自動読み込み。上書き時だけパスを指定",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
TRAIN_BACKEND_CHOICES = ["auto", "cuda", "xpu", "mps", "cpu"]
|
|
248
|
+
|
|
249
|
+
TRAIN_BACKEND_LABELS = {
|
|
250
|
+
"auto": {
|
|
251
|
+
"zh-Hans": "自动 (默认最快)",
|
|
252
|
+
"zh-Hant": "自動 (預設最快)",
|
|
253
|
+
"en": "Auto (fastest default)",
|
|
254
|
+
"ja": "自動 (最速を既定)",
|
|
255
|
+
},
|
|
256
|
+
"cuda": {
|
|
257
|
+
"zh-Hans": "CUDA",
|
|
258
|
+
"zh-Hant": "CUDA",
|
|
259
|
+
"en": "CUDA",
|
|
260
|
+
"ja": "CUDA",
|
|
261
|
+
},
|
|
262
|
+
"mps": {
|
|
263
|
+
"zh-Hans": "MPS",
|
|
264
|
+
"zh-Hant": "MPS",
|
|
265
|
+
"en": "MPS",
|
|
266
|
+
"ja": "MPS",
|
|
267
|
+
},
|
|
268
|
+
"xpu": {
|
|
269
|
+
"zh-Hans": "XPU",
|
|
270
|
+
"zh-Hant": "XPU",
|
|
271
|
+
"en": "XPU",
|
|
272
|
+
"ja": "XPU",
|
|
273
|
+
},
|
|
274
|
+
"cpu": {
|
|
275
|
+
"zh-Hans": "CPU",
|
|
276
|
+
"zh-Hant": "CPU",
|
|
277
|
+
"en": "CPU",
|
|
278
|
+
"ja": "CPU",
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _stage_text(stage_map: dict[str, str], mode: str, lang: str | None = None) -> str:
|
|
284
|
+
lang = lang or get_current_lang()
|
|
285
|
+
per_stage = stage_map.get(mode, stage_map["pretrain"])
|
|
286
|
+
return per_stage.get(lang, per_stage["en"])
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _stage_init_placeholder(mode: str, lang: str | None = None) -> str:
|
|
290
|
+
lang = lang or get_current_lang()
|
|
291
|
+
if mode == "pretrain":
|
|
292
|
+
return PRETRAIN_INIT_PLACEHOLDER.get(lang, PRETRAIN_INIT_PLACEHOLDER["en"])
|
|
293
|
+
upstream = STAGE_DEFAULT_INIT.get(mode, "chronos")
|
|
294
|
+
template = UPSTREAM_INIT_PLACEHOLDER.get(lang, UPSTREAM_INIT_PLACEHOLDER["en"])
|
|
295
|
+
return template.format(prefix=upstream)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _normalize_stage_init_value(current_init_weight: str | None) -> str:
|
|
299
|
+
current_init = (current_init_weight or "").strip()
|
|
300
|
+
if current_init in DEFAULT_STAGE_INIT_VALUES:
|
|
301
|
+
return ""
|
|
302
|
+
return current_init
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _distill_teacher_placeholder(lang: str | None = None) -> str:
|
|
306
|
+
lang = lang or get_current_lang()
|
|
307
|
+
return DISTILL_TEACHER_PLACEHOLDER.get(lang, DISTILL_TEACHER_PLACEHOLDER["en"])
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _available_train_backend_choices() -> list[str]:
|
|
311
|
+
available = set(training_available())
|
|
312
|
+
return ["auto"] + [name for name in TRAIN_BACKEND_CHOICES[1:] if name in available]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _train_backend_label(name: str, lang: str | None = None) -> str:
|
|
316
|
+
lang = lang or get_current_lang()
|
|
317
|
+
labels = TRAIN_BACKEND_LABELS.get(name, TRAIN_BACKEND_LABELS["auto"])
|
|
318
|
+
return labels.get(lang, labels["en"])
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _train_backend_dropdown_choices(lang: str | None = None) -> list[tuple[str, str]]:
|
|
322
|
+
return [
|
|
323
|
+
(_train_backend_label(name, lang), name)
|
|
324
|
+
for name in _available_train_backend_choices()
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _default_train_backend_value() -> str:
|
|
329
|
+
return "auto"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _normalize_dtype_for_trainer(dtype: str | None) -> str:
|
|
333
|
+
dt = (dtype or "fp16").strip().lower()
|
|
334
|
+
if dt in {"bf16", "bfloat16"}:
|
|
335
|
+
return "bfloat16"
|
|
336
|
+
return "float16"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@contextmanager
|
|
340
|
+
def _trainer_logger_sink(put_line):
|
|
341
|
+
from trainer import trainer_utils as _tu # type: ignore
|
|
342
|
+
|
|
343
|
+
orig_logger = _tu.Logger
|
|
344
|
+
|
|
345
|
+
def _hook(content):
|
|
346
|
+
line = str(content)
|
|
347
|
+
put_line(line)
|
|
348
|
+
try:
|
|
349
|
+
orig_logger(content)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
_tu.Logger = _hook
|
|
354
|
+
try:
|
|
355
|
+
yield
|
|
356
|
+
finally:
|
|
357
|
+
_tu.Logger = orig_logger
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _sniff_checkpoint(path: str) -> dict:
|
|
361
|
+
import os as _os
|
|
362
|
+
if not path or not _os.path.exists(path):
|
|
363
|
+
return {}
|
|
364
|
+
try:
|
|
365
|
+
import torch as _t
|
|
366
|
+
sd = _t.load(path, map_location="cpu")
|
|
367
|
+
if not isinstance(sd, dict):
|
|
368
|
+
return {}
|
|
369
|
+
out = {}
|
|
370
|
+
|
|
371
|
+
embed = sd.get("model.embed_tokens.weight")
|
|
372
|
+
if embed is not None:
|
|
373
|
+
out["vocab_size"], out["hidden_size"] = int(embed.shape[0]), int(embed.shape[1])
|
|
374
|
+
|
|
375
|
+
layer_idxs = set()
|
|
376
|
+
for k in sd.keys():
|
|
377
|
+
if k.startswith("model.layers."):
|
|
378
|
+
try:
|
|
379
|
+
layer_idxs.add(int(k.split(".")[2]))
|
|
380
|
+
except (ValueError, IndexError):
|
|
381
|
+
pass
|
|
382
|
+
if layer_idxs:
|
|
383
|
+
out["num_hidden_layers"] = max(layer_idxs) + 1
|
|
384
|
+
|
|
385
|
+
expert_idxs = set()
|
|
386
|
+
for k in sd.keys():
|
|
387
|
+
if k.startswith("model.layers.0.mlp.experts."):
|
|
388
|
+
try:
|
|
389
|
+
expert_idxs.add(int(k.split(".")[5]))
|
|
390
|
+
except (ValueError, IndexError):
|
|
391
|
+
pass
|
|
392
|
+
if expert_idxs:
|
|
393
|
+
out["num_experts"] = max(expert_idxs) + 1
|
|
394
|
+
|
|
395
|
+
gate = sd.get("model.layers.0.mlp.experts.0.gate_proj.weight")
|
|
396
|
+
if gate is not None:
|
|
397
|
+
out["moe_intermediate_size"] = int(gate.shape[0])
|
|
398
|
+
|
|
399
|
+
qw = sd.get("model.layers.0.self_attn.q_proj.weight")
|
|
400
|
+
kw = sd.get("model.layers.0.self_attn.k_proj.weight")
|
|
401
|
+
if qw is not None and kw is not None:
|
|
402
|
+
out["num_attention_heads_total_dim"] = int(qw.shape[0])
|
|
403
|
+
out["num_kv_heads_total_dim"] = int(kw.shape[0])
|
|
404
|
+
|
|
405
|
+
lookahead_proj = sd.get("model.lookahead_router.proj.2.weight")
|
|
406
|
+
if lookahead_proj is not None and out.get("num_experts"):
|
|
407
|
+
total = int(lookahead_proj.shape[0])
|
|
408
|
+
n_exp = int(out["num_experts"])
|
|
409
|
+
if n_exp > 0 and total % n_exp == 0:
|
|
410
|
+
# The router predicts current-token routing plus K future
|
|
411
|
+
# steps, so the saved output rows are (lookahead_steps + 1)
|
|
412
|
+
# * num_experts. Do not report the +1 as user topology.
|
|
413
|
+
out["lookahead_steps"] = max(0, total // n_exp - 1)
|
|
414
|
+
|
|
415
|
+
qnope = sd.get("model.layers.0.self_attn.q_nope_proj.weight")
|
|
416
|
+
qrope = sd.get("model.layers.0.self_attn.q_rope_proj.weight")
|
|
417
|
+
kvdown = sd.get("model.layers.0.self_attn.kv_down_proj.weight")
|
|
418
|
+
vproj = sd.get("model.layers.0.self_attn.v_proj.weight")
|
|
419
|
+
if qnope is not None and qrope is not None and out.get("hidden_size"):
|
|
420
|
+
candidates = [
|
|
421
|
+
n for n in range(1, out["hidden_size"] + 1)
|
|
422
|
+
if out["hidden_size"] % n == 0
|
|
423
|
+
and int(qrope.shape[0]) % n == 0
|
|
424
|
+
and int(qnope.shape[0]) % n == 0
|
|
425
|
+
]
|
|
426
|
+
if candidates:
|
|
427
|
+
preferred = 8 if 8 in candidates else max(candidates)
|
|
428
|
+
head_dim = out["hidden_size"] // preferred
|
|
429
|
+
rope_dim = int(qrope.shape[0]) // preferred
|
|
430
|
+
nope_dim = int(qnope.shape[0]) // preferred
|
|
431
|
+
if rope_dim + nope_dim == head_dim:
|
|
432
|
+
out["num_attention_heads"] = preferred
|
|
433
|
+
out["rope_dim"] = rope_dim
|
|
434
|
+
if kvdown is not None:
|
|
435
|
+
out["kv_latent_dim"] = int(kvdown.shape[0])
|
|
436
|
+
if vproj is not None and out.get("kv_latent_dim"):
|
|
437
|
+
head_dim = out.get("hidden_size", 0) // max(out.get("num_attention_heads", 8), 1)
|
|
438
|
+
if head_dim > 0:
|
|
439
|
+
out["num_key_value_heads"] = max(1, int(vproj.shape[0]) // head_dim)
|
|
440
|
+
|
|
441
|
+
return out
|
|
442
|
+
except Exception:
|
|
443
|
+
return {}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ── Background trainer thread ─────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
class TrainSession:
|
|
449
|
+
def __init__(self):
|
|
450
|
+
self._stop = threading.Event()
|
|
451
|
+
self._thread = None
|
|
452
|
+
self._log_q: queue.Queue = queue.Queue(maxsize=5000)
|
|
453
|
+
self.status = "idle"
|
|
454
|
+
self.step = 0
|
|
455
|
+
self.loss = None
|
|
456
|
+
# Separate metric history for charts
|
|
457
|
+
self._metrics: list[dict] = [] # [{step, total, ce, aux, temporal, tps}]
|
|
458
|
+
self._metric_lock = threading.Lock()
|
|
459
|
+
# Progress + ETA — set when training kicks off
|
|
460
|
+
self.total_steps: int = 0
|
|
461
|
+
self.t_start: float = 0.0
|
|
462
|
+
# M8a: live generation sample (updated each save_interval).
|
|
463
|
+
self.last_sample: str = ""
|
|
464
|
+
self.last_sample_step: int = 0
|
|
465
|
+
|
|
466
|
+
def is_running(self):
|
|
467
|
+
return self._thread is not None and self._thread.is_alive()
|
|
468
|
+
|
|
469
|
+
def start(self, cfg: dict, mode: str):
|
|
470
|
+
if self.is_running():
|
|
471
|
+
return
|
|
472
|
+
self._stop.clear()
|
|
473
|
+
self.status = "running"
|
|
474
|
+
self.step = 0
|
|
475
|
+
self.loss = None
|
|
476
|
+
self._metrics.clear()
|
|
477
|
+
self.total_steps = 0
|
|
478
|
+
self.t_start = time.monotonic()
|
|
479
|
+
self._thread = threading.Thread(
|
|
480
|
+
target=self._run, args=(cfg, mode), daemon=True
|
|
481
|
+
)
|
|
482
|
+
self._thread.start()
|
|
483
|
+
|
|
484
|
+
def stop(self):
|
|
485
|
+
self._stop.set()
|
|
486
|
+
self.status = "stopped"
|
|
487
|
+
|
|
488
|
+
def _put(self, msg: str):
|
|
489
|
+
try:
|
|
490
|
+
self._log_q.put_nowait(msg)
|
|
491
|
+
except queue.Full:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
def _put_metric(self, m: dict):
|
|
495
|
+
with self._metric_lock:
|
|
496
|
+
self._metrics.append(m)
|
|
497
|
+
|
|
498
|
+
def drain_log(self):
|
|
499
|
+
"""Return all pending log lines as a single string (no newline prefix)."""
|
|
500
|
+
lines = []
|
|
501
|
+
while True:
|
|
502
|
+
try:
|
|
503
|
+
lines.append(self._log_q.get_nowait())
|
|
504
|
+
except queue.Empty:
|
|
505
|
+
break
|
|
506
|
+
return "\n".join(lines)
|
|
507
|
+
|
|
508
|
+
def get_metrics(self):
|
|
509
|
+
with self._metric_lock:
|
|
510
|
+
return list(self._metrics)
|
|
511
|
+
|
|
512
|
+
def _stage_checkpoint_path(self, save_dir: str, mode: str, hidden_size: int) -> str:
|
|
513
|
+
prefix = STAGE_CHECKPOINT_PREFIX.get(mode, "chronos")
|
|
514
|
+
return os.path.join(save_dir, f"{prefix}_{hidden_size}_moe.pth")
|
|
515
|
+
|
|
516
|
+
def _default_init_path(self, save_dir: str, mode: str, hidden_size: int) -> str:
|
|
517
|
+
upstream = STAGE_DEFAULT_INIT.get(mode)
|
|
518
|
+
if not upstream:
|
|
519
|
+
return ""
|
|
520
|
+
return os.path.join(save_dir, f"{upstream}_{hidden_size}_moe.pth")
|
|
521
|
+
|
|
522
|
+
def _resolve_default_init_path(self, save_dir: str, mode: str, hidden_size: int) -> str:
|
|
523
|
+
exact = self._default_init_path(save_dir, mode, hidden_size)
|
|
524
|
+
if os.path.exists(exact):
|
|
525
|
+
return exact
|
|
526
|
+
upstream = STAGE_DEFAULT_INIT.get(mode)
|
|
527
|
+
if not upstream:
|
|
528
|
+
return exact
|
|
529
|
+
candidates = [
|
|
530
|
+
p for p in glob.glob(os.path.join(save_dir, f"{upstream}_*_moe.pth"))
|
|
531
|
+
if os.path.isfile(p)
|
|
532
|
+
]
|
|
533
|
+
if not candidates:
|
|
534
|
+
return exact
|
|
535
|
+
return max(candidates, key=os.path.getmtime)
|
|
536
|
+
|
|
537
|
+
def _topology_mismatches(self, sniffed: dict, model_cfg_kwargs: dict) -> list[str]:
|
|
538
|
+
mismatches = []
|
|
539
|
+
for k in CHECKPOINT_TOPOLOGY_KEYS:
|
|
540
|
+
if k in sniffed and k in model_cfg_kwargs and int(sniffed[k]) != int(model_cfg_kwargs[k]):
|
|
541
|
+
mismatches.append(f"{k}: ckpt={sniffed[k]} != ui={model_cfg_kwargs[k]}")
|
|
542
|
+
return mismatches
|
|
543
|
+
|
|
544
|
+
def _adopt_checkpoint_topology(self, model_cfg_kwargs: dict, sniffed: dict) -> dict:
|
|
545
|
+
adopted = {}
|
|
546
|
+
for key in CHECKPOINT_TOPOLOGY_KEYS:
|
|
547
|
+
if key not in sniffed:
|
|
548
|
+
continue
|
|
549
|
+
current = model_cfg_kwargs.get(key)
|
|
550
|
+
value = sniffed[key]
|
|
551
|
+
if current is None or int(current) != int(value):
|
|
552
|
+
adopted[key] = value
|
|
553
|
+
model_cfg_kwargs[key] = value
|
|
554
|
+
if "moe_intermediate_size" in sniffed:
|
|
555
|
+
model_cfg_kwargs["intermediate_size"] = sniffed["moe_intermediate_size"]
|
|
556
|
+
return adopted
|
|
557
|
+
|
|
558
|
+
def _build_stage_args(self, cfg: dict, mode: str, save_dir: str, hidden_size: int):
|
|
559
|
+
reward_spec = (cfg.get("reward_spec") or "toy").strip() or "toy"
|
|
560
|
+
teacher_path = (cfg.get("teacher_path") or "").strip()
|
|
561
|
+
return SimpleNamespace(
|
|
562
|
+
device=cfg.get("device", "cpu"),
|
|
563
|
+
dtype=_normalize_dtype_for_trainer(cfg.get("dtype")),
|
|
564
|
+
learning_rate=float(cfg.get("learning_rate", 5e-4)),
|
|
565
|
+
accumulation_steps=max(1, int(cfg.get("accumulation_steps", 1))),
|
|
566
|
+
grad_clip=float(cfg.get("grad_clip", 1.0)),
|
|
567
|
+
log_interval=max(1, int(cfg.get("log_interval", 10))),
|
|
568
|
+
save_interval=max(1, int(cfg.get("save_interval", 500))),
|
|
569
|
+
save_dir=save_dir,
|
|
570
|
+
epochs=max(1, int(cfg.get("epochs", 1))),
|
|
571
|
+
batch_size=max(1, int(cfg.get("batch_size", 1))),
|
|
572
|
+
max_seq_len=max(8, int(cfg.get("max_seq_len", 512))),
|
|
573
|
+
max_gen_len=max(1, int(cfg.get("max_gen_len", 24))),
|
|
574
|
+
num_generations=max(1, int(cfg.get("num_generations", 4))),
|
|
575
|
+
temperature=float(cfg.get("temperature", 1.0)),
|
|
576
|
+
beta=float(cfg.get("beta", 0.1)),
|
|
577
|
+
alpha=float(cfg.get("alpha", 0.7)),
|
|
578
|
+
lambda_or=float(cfg.get("lambda_or", 0.1)),
|
|
579
|
+
lambda_router_anchor=float(cfg.get("lambda_router_anchor", 0.0)),
|
|
580
|
+
reward=reward_spec,
|
|
581
|
+
teacher_path=teacher_path or os.path.join(save_dir, f"sft_{hidden_size}_moe.pth"),
|
|
582
|
+
hidden_size=hidden_size,
|
|
583
|
+
num_hidden_layers=int(cfg.get("num_hidden_layers", 8)),
|
|
584
|
+
num_experts=int(cfg.get("num_experts", 4)),
|
|
585
|
+
steps=int(cfg.get("max_steps", 0)) or None,
|
|
586
|
+
weight_decay=float(cfg.get("weight_decay", 0.01)),
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def _record_stage_metric(self, mode: str, step: int, result, tps: float = 0.0):
|
|
590
|
+
meta = STAGE_CHART_META.get(mode, STAGE_CHART_META["pretrain"])
|
|
591
|
+
if isinstance(result, tuple):
|
|
592
|
+
if mode == "sft":
|
|
593
|
+
total, ce_loss, aux_loss, lookahead, anchor = result
|
|
594
|
+
metric = {
|
|
595
|
+
"total": total,
|
|
596
|
+
"primary": ce_loss,
|
|
597
|
+
"secondary": lookahead,
|
|
598
|
+
"tertiary": anchor,
|
|
599
|
+
"ce": ce_loss,
|
|
600
|
+
"aux": lookahead,
|
|
601
|
+
"temporal": anchor,
|
|
602
|
+
}
|
|
603
|
+
elif mode == "dpo":
|
|
604
|
+
total, pref_loss, _aux_loss, lookahead, anchor = result
|
|
605
|
+
metric = {
|
|
606
|
+
"total": total,
|
|
607
|
+
"primary": pref_loss,
|
|
608
|
+
"secondary": anchor,
|
|
609
|
+
"tertiary": lookahead,
|
|
610
|
+
"ce": pref_loss,
|
|
611
|
+
"aux": anchor,
|
|
612
|
+
"temporal": lookahead,
|
|
613
|
+
}
|
|
614
|
+
elif mode == "orpo":
|
|
615
|
+
total, chosen_nll, or_term, _lookahead, anchor = result
|
|
616
|
+
metric = {
|
|
617
|
+
"total": total,
|
|
618
|
+
"primary": chosen_nll,
|
|
619
|
+
"secondary": or_term,
|
|
620
|
+
"tertiary": anchor,
|
|
621
|
+
"ce": chosen_nll,
|
|
622
|
+
"aux": or_term,
|
|
623
|
+
"temporal": anchor,
|
|
624
|
+
}
|
|
625
|
+
elif mode == "distill":
|
|
626
|
+
total, kd_loss, ce_loss, _lookahead, anchor = result
|
|
627
|
+
metric = {
|
|
628
|
+
"total": total,
|
|
629
|
+
"primary": kd_loss,
|
|
630
|
+
"secondary": ce_loss,
|
|
631
|
+
"tertiary": anchor,
|
|
632
|
+
"ce": kd_loss,
|
|
633
|
+
"aux": ce_loss,
|
|
634
|
+
"temporal": anchor,
|
|
635
|
+
}
|
|
636
|
+
else:
|
|
637
|
+
return
|
|
638
|
+
elif isinstance(result, dict) and mode == "grpo":
|
|
639
|
+
metric = {
|
|
640
|
+
"total": result.get("loss", 0.0),
|
|
641
|
+
"primary": result.get("pg_loss", 0.0),
|
|
642
|
+
"secondary": result.get("kl", 0.0),
|
|
643
|
+
"tertiary": result.get("mean_reward", 0.0),
|
|
644
|
+
"ce": result.get("pg_loss", 0.0),
|
|
645
|
+
"aux": result.get("kl", 0.0),
|
|
646
|
+
"temporal": result.get("mean_reward", 0.0),
|
|
647
|
+
}
|
|
648
|
+
else:
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
if mode == "pretrain":
|
|
652
|
+
metric.setdefault("primary", metric.get("ce", 0.0))
|
|
653
|
+
metric.setdefault("secondary", metric.get("aux", 0.0))
|
|
654
|
+
metric.setdefault("tertiary", metric.get("temporal", 0.0))
|
|
655
|
+
|
|
656
|
+
metric.update({"step": int(step), "tps": float(tps)})
|
|
657
|
+
self.step = int(step)
|
|
658
|
+
self.loss = float(metric["total"])
|
|
659
|
+
self._put_metric(metric)
|
|
660
|
+
self._put(
|
|
661
|
+
f"[{mode.upper()} summary] step={step} total={metric['total']:.4f} "
|
|
662
|
+
f"{meta['primary']}={metric.get('primary', 0.0):.4f} "
|
|
663
|
+
f"{meta['secondary']}={metric.get('secondary', 0.0):.4f} "
|
|
664
|
+
f"{meta['tertiary']}={metric.get('tertiary', 0.0):.4f} "
|
|
665
|
+
f"tps={tps:.2f}"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def _generate_live_sample(self, model, device: str, cfg: dict, mode: str):
|
|
669
|
+
import torch
|
|
670
|
+
|
|
671
|
+
sp = (cfg.get("sample_prompt") or "").strip()
|
|
672
|
+
if not sp:
|
|
673
|
+
sp = _stage_sample_prompt(mode)
|
|
674
|
+
tokenizer = self._load_tokenizer()
|
|
675
|
+
ids = tokenizer.encode(sp, return_tensors="pt").to(device)
|
|
676
|
+
was_training = model.training
|
|
677
|
+
model.eval()
|
|
678
|
+
with torch.no_grad():
|
|
679
|
+
gen = ids.clone()
|
|
680
|
+
for _ in range(80):
|
|
681
|
+
o, _ = model(gen, use_cache=False)
|
|
682
|
+
nxt = o.logits[:, -1, :].argmax(dim=-1, keepdim=True)
|
|
683
|
+
gen = torch.cat([gen, nxt], dim=-1)
|
|
684
|
+
if int(nxt.item()) == (tokenizer.eos_token_id or -1):
|
|
685
|
+
break
|
|
686
|
+
txt = tokenizer.decode(gen[0].tolist(), skip_special_tokens=True)
|
|
687
|
+
if was_training:
|
|
688
|
+
model.train()
|
|
689
|
+
self.last_sample = txt
|
|
690
|
+
self.last_sample_step = self.step or self.last_sample_step
|
|
691
|
+
self._put(f"[sample @ step {self.last_sample_step or 0}] {txt[:200]}")
|
|
692
|
+
|
|
693
|
+
def _run(self, cfg: dict, mode: str):
|
|
694
|
+
t_start = time.monotonic()
|
|
695
|
+
try:
|
|
696
|
+
import torch
|
|
697
|
+
from torch.utils.data import DataLoader
|
|
698
|
+
|
|
699
|
+
from chronos.model.config import ChronosConfig
|
|
700
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
701
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
702
|
+
|
|
703
|
+
self._put(f"[{mode.upper()}] Building model...")
|
|
704
|
+
# Pass the FULL config dict so user-set arch overrides (e.g.
|
|
705
|
+
# moe_intermediate_size, num_attention_heads, vocab_size) are
|
|
706
|
+
# actually honored. Previously this method only forwarded a
|
|
707
|
+
# hardcoded subset, so the trainer silently used minimind's
|
|
708
|
+
# auto-derived intermediate_size while the UI estimator showed
|
|
709
|
+
# the user's slider — causing 10x param-count mismatches.
|
|
710
|
+
model_cfg_kwargs = {
|
|
711
|
+
"hidden_size": cfg.get("hidden_size", 512),
|
|
712
|
+
"num_hidden_layers": cfg.get("num_hidden_layers", 8),
|
|
713
|
+
"num_experts": cfg.get("num_experts", 4),
|
|
714
|
+
"num_experts_per_tok": cfg.get("num_experts_per_tok", 1),
|
|
715
|
+
"num_shared_experts": cfg.get("num_shared_experts", 1),
|
|
716
|
+
"lookahead_steps": cfg.get("lookahead_steps", 2),
|
|
717
|
+
"kv_latent_dim": cfg.get("kv_latent_dim", 64),
|
|
718
|
+
"sliding_window_size": cfg.get("sliding_window_size", 2048),
|
|
719
|
+
"lambda_balance": cfg.get("lambda_balance", 5e-4),
|
|
720
|
+
"lambda_temporal": cfg.get("lambda_temporal", 1e-3),
|
|
721
|
+
"vram_budget_gb": cfg.get("vram_budget_gb", 4.0),
|
|
722
|
+
"use_hybrid_attention": cfg.get("use_hybrid_attention", True),
|
|
723
|
+
"use_moe": True,
|
|
724
|
+
}
|
|
725
|
+
# Optional overrides: only forward when the user actually set them.
|
|
726
|
+
# For the "auto" sentinel fields (0 = let MiniMindConfig derive
|
|
727
|
+
# it), we skip forwarding when value is 0 so ceil(H·π/64)·64
|
|
728
|
+
# takes effect instead of Linear(H, 0) crashing MPS init.
|
|
729
|
+
AUTO_SENTINEL_KEYS = {"intermediate_size", "moe_intermediate_size"}
|
|
730
|
+
for opt_key, cfg_key in [
|
|
731
|
+
("num_attention_heads", "num_attention_heads"),
|
|
732
|
+
("num_key_value_heads", "num_key_value_heads"),
|
|
733
|
+
("rope_dim", "rope_dim"),
|
|
734
|
+
("vocab_size", "vocab_size"),
|
|
735
|
+
("max_position_embeddings", "max_seq_len"),
|
|
736
|
+
("intermediate_size", "moe_intermediate_size"),
|
|
737
|
+
("moe_intermediate_size", "moe_intermediate_size"),
|
|
738
|
+
("lambda_lookahead", "lambda_lookahead"),
|
|
739
|
+
("lambda_router_anchor", "lambda_router_anchor"),
|
|
740
|
+
("pinned_memory_max_fraction", "pinned_memory_max_fraction"),
|
|
741
|
+
("storage_format", "storage_format"),
|
|
742
|
+
("tie_word_embeddings", "tie_word_embeddings"),
|
|
743
|
+
]:
|
|
744
|
+
if cfg_key not in cfg:
|
|
745
|
+
continue
|
|
746
|
+
val = cfg[cfg_key]
|
|
747
|
+
if val in (None, ""):
|
|
748
|
+
continue
|
|
749
|
+
if opt_key in AUTO_SENTINEL_KEYS and val == 0:
|
|
750
|
+
continue
|
|
751
|
+
model_cfg_kwargs[opt_key] = val
|
|
752
|
+
save_dir = cfg.get("save_dir", "./out")
|
|
753
|
+
init_weight = (cfg.get("init_weight") or "").strip()
|
|
754
|
+
load_path = ""
|
|
755
|
+
init_sniffed = {}
|
|
756
|
+
if mode != "pretrain":
|
|
757
|
+
load_path = init_weight or self._resolve_default_init_path(
|
|
758
|
+
save_dir, mode, int(model_cfg_kwargs["hidden_size"])
|
|
759
|
+
)
|
|
760
|
+
if os.path.exists(load_path):
|
|
761
|
+
init_sniffed = _sniff_checkpoint(load_path)
|
|
762
|
+
adopted = self._adopt_checkpoint_topology(model_cfg_kwargs, init_sniffed)
|
|
763
|
+
if adopted:
|
|
764
|
+
summary = ", ".join(f"{k}={v}" for k, v in adopted.items())
|
|
765
|
+
self._put(
|
|
766
|
+
f"[{mode.upper()}] Adopted topology from init checkpoint "
|
|
767
|
+
f"{load_path}: {summary}"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
model_cfg = ChronosConfig(**model_cfg_kwargs)
|
|
771
|
+
model = ChronosForCausalLM(model_cfg)
|
|
772
|
+
params_m = sum(p.numel() for p in model.parameters()) / 1e6
|
|
773
|
+
self._put(f"Model: {params_m:.1f}M params "
|
|
774
|
+
f"(H={model_cfg.hidden_size}, L={model_cfg.num_hidden_layers}, "
|
|
775
|
+
f"E={model_cfg.num_experts}, ffn={model_cfg.intermediate_size}, "
|
|
776
|
+
f"vocab={model_cfg.vocab_size})")
|
|
777
|
+
|
|
778
|
+
ckp_path = self._stage_checkpoint_path(save_dir, mode, model_cfg.hidden_size)
|
|
779
|
+
|
|
780
|
+
# Init-weight resolution:
|
|
781
|
+
# - pretrain: optionally resume from its own checkpoint if present
|
|
782
|
+
# - other stages: require an upstream weight (explicit init_weight
|
|
783
|
+
# or the stage's default predecessor checkpoint).
|
|
784
|
+
if mode == "pretrain":
|
|
785
|
+
resume_path = init_weight or ckp_path
|
|
786
|
+
if os.path.exists(resume_path):
|
|
787
|
+
sniffed = _sniff_checkpoint(resume_path)
|
|
788
|
+
mismatches = self._topology_mismatches(sniffed, model_cfg_kwargs)
|
|
789
|
+
if mismatches:
|
|
790
|
+
self._put(
|
|
791
|
+
"Resume checkpoint exists but does not match current UI config; "
|
|
792
|
+
"starting fresh instead.\n " + "\n ".join(mismatches)
|
|
793
|
+
)
|
|
794
|
+
else:
|
|
795
|
+
weights = torch.load(resume_path, map_location="cpu")
|
|
796
|
+
model.load_state_dict(weights, strict=False)
|
|
797
|
+
self._put(f"Resumed from {resume_path}")
|
|
798
|
+
else:
|
|
799
|
+
self._put("Pretraining from random init")
|
|
800
|
+
else:
|
|
801
|
+
if not os.path.exists(load_path):
|
|
802
|
+
raise FileNotFoundError(
|
|
803
|
+
f"[{mode.upper()}] requires an upstream checkpoint to initialize from. "
|
|
804
|
+
f"Tried: {load_path}\n"
|
|
805
|
+
f"Set 'init_weight' in the Train tab to point at a valid .pth, "
|
|
806
|
+
f"or run the prior stage first."
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
sniffed = init_sniffed or _sniff_checkpoint(load_path)
|
|
810
|
+
mismatch_hints = self._topology_mismatches(sniffed, model_cfg_kwargs)
|
|
811
|
+
if mismatch_hints:
|
|
812
|
+
raise RuntimeError(
|
|
813
|
+
f"[{mode.upper()}] init checkpoint topology does not match current UI config:\n "
|
|
814
|
+
+ "\n ".join(mismatch_hints)
|
|
815
|
+
+ "\nLoad the matching preset/config first, or switch init_weight to the correct checkpoint."
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
weights = torch.load(load_path, map_location="cpu")
|
|
819
|
+
model.load_state_dict(weights, strict=False)
|
|
820
|
+
self._put(f"[{mode.upper()}] Initialized from {load_path}")
|
|
821
|
+
|
|
822
|
+
requested_backend = (cfg.get("train_backend") or "auto").strip().lower() or "auto"
|
|
823
|
+
resolved_backend, device = resolve_training_device(requested_backend)
|
|
824
|
+
cfg["train_backend"] = requested_backend
|
|
825
|
+
cfg["device"] = device
|
|
826
|
+
self._put(f"Training backend: {resolved_backend} (requested: {requested_backend})")
|
|
827
|
+
self._put(f"Device: {device}")
|
|
828
|
+
model = model.to(device)
|
|
829
|
+
from chronos.trainer.optim_utils import build_optimizer, get_lr, apply_lr
|
|
830
|
+
|
|
831
|
+
data_path = cfg.get("data_path", "")
|
|
832
|
+
max_seq_len = cfg.get("max_seq_len", 512)
|
|
833
|
+
batch_size = cfg.get("batch_size", 4)
|
|
834
|
+
accum = max(1, int(cfg.get("accumulation_steps", 8)))
|
|
835
|
+
epochs = cfg.get("epochs", 1)
|
|
836
|
+
save_interval = cfg.get("save_interval", 500)
|
|
837
|
+
log_interval = max(1, cfg.get("log_interval", 10))
|
|
838
|
+
val_ratio = float(cfg.get("val_ratio", 0.05))
|
|
839
|
+
max_steps = int(cfg.get("max_steps", 0)) or None # 0 => no cap
|
|
840
|
+
base_lr = float(cfg.get("learning_rate", 5e-4))
|
|
841
|
+
weight_decay = float(cfg.get("weight_decay", 0.01))
|
|
842
|
+
optimizer = build_optimizer(model, lr=base_lr, weight_decay=weight_decay)
|
|
843
|
+
|
|
844
|
+
if not data_path or not os.path.exists(data_path):
|
|
845
|
+
self._put("No dataset found — running synthetic smoke test (50 steps)")
|
|
846
|
+
loader = self._synthetic_loader(model_cfg.vocab_size, max_seq_len, batch_size, n=50)
|
|
847
|
+
val_loader = None
|
|
848
|
+
else:
|
|
849
|
+
tokenizer = self._load_tokenizer()
|
|
850
|
+
from chronos.data.flexible_dataset import (
|
|
851
|
+
FlexibleDataset, StreamingSFTDataset, StreamingDPODataset,
|
|
852
|
+
)
|
|
853
|
+
if mode == "pretrain":
|
|
854
|
+
full_ds = FlexibleDataset(data_path, tokenizer, max_length=max_seq_len)
|
|
855
|
+
elif mode in {"sft", "distill"}:
|
|
856
|
+
full_ds = StreamingSFTDataset(data_path, tokenizer, max_length=max_seq_len)
|
|
857
|
+
elif mode in {"dpo", "orpo"}:
|
|
858
|
+
full_ds = StreamingDPODataset(data_path, tokenizer, max_length=max_seq_len)
|
|
859
|
+
else:
|
|
860
|
+
full_ds = None
|
|
861
|
+
# Deterministic val split: every 1/val_ratio-th record goes to val
|
|
862
|
+
# (idx-modulo split → reproducible without rescanning the file).
|
|
863
|
+
if full_ds is not None and val_ratio > 0 and len(full_ds) >= 20:
|
|
864
|
+
stride = max(2, int(round(1.0 / val_ratio)))
|
|
865
|
+
val_idx = [i for i in range(len(full_ds)) if i % stride == 0]
|
|
866
|
+
train_idx = [i for i in range(len(full_ds)) if i % stride != 0]
|
|
867
|
+
from torch.utils.data import Subset
|
|
868
|
+
train_ds = Subset(full_ds, train_idx)
|
|
869
|
+
val_ds = Subset(full_ds, val_idx)
|
|
870
|
+
self._put(f"Split: train={len(train_ds)} val={len(val_ds)}")
|
|
871
|
+
elif full_ds is not None:
|
|
872
|
+
train_ds, val_ds = full_ds, None
|
|
873
|
+
else:
|
|
874
|
+
train_ds, val_ds = None, None
|
|
875
|
+
loader = (
|
|
876
|
+
DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=False)
|
|
877
|
+
if train_ds is not None else None
|
|
878
|
+
)
|
|
879
|
+
val_loader = (
|
|
880
|
+
DataLoader(val_ds, batch_size=batch_size, shuffle=False,
|
|
881
|
+
num_workers=0, pin_memory=False)
|
|
882
|
+
if val_ds is not None else None
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
if mode in {"grpo"}:
|
|
886
|
+
tokenizer = self._load_tokenizer()
|
|
887
|
+
from chronos.trainer.grpo_trainer import load_grpo_prompts
|
|
888
|
+
prompts = load_grpo_prompts(data_path, max_prompts=max_steps)
|
|
889
|
+
if not prompts:
|
|
890
|
+
raise ValueError(f"[{mode.upper()}] no prompts found in {data_path}")
|
|
891
|
+
planned = max(1, len(prompts) * epochs)
|
|
892
|
+
self.total_steps = min(planned, max_steps) if (max_steps and planned) else planned
|
|
893
|
+
else:
|
|
894
|
+
prompts = None
|
|
895
|
+
|
|
896
|
+
model.train()
|
|
897
|
+
global_step = 0
|
|
898
|
+
step_t = time.monotonic()
|
|
899
|
+
best_val = float("inf")
|
|
900
|
+
best_path = ckp_path.replace(".pth", ".best.pth")
|
|
901
|
+
# Steps remaining for ETA / progress bar — respect max_steps cap.
|
|
902
|
+
try:
|
|
903
|
+
planned = max(1, len(loader) * epochs) if loader is not None else 0
|
|
904
|
+
except Exception:
|
|
905
|
+
planned = 0
|
|
906
|
+
if mode != "grpo":
|
|
907
|
+
self.total_steps = min(planned, max_steps) if (max_steps and planned) else (planned or (max_steps or 0))
|
|
908
|
+
|
|
909
|
+
stage_args = self._build_stage_args(cfg, mode, save_dir, model_cfg.hidden_size)
|
|
910
|
+
stage_args.device = device
|
|
911
|
+
|
|
912
|
+
if mode in {"sft", "dpo", "orpo", "grpo", "distill"}:
|
|
913
|
+
tokenizer = self._load_tokenizer()
|
|
914
|
+
|
|
915
|
+
def _eval_val() -> float:
|
|
916
|
+
if val_loader is None:
|
|
917
|
+
return float("nan")
|
|
918
|
+
model.eval()
|
|
919
|
+
tot, n = 0.0, 0
|
|
920
|
+
with torch.no_grad():
|
|
921
|
+
for k, (vx, vy) in enumerate(val_loader):
|
|
922
|
+
if k >= 50: # cap eval cost
|
|
923
|
+
break
|
|
924
|
+
vx = vx.to(device); vy = vy.to(device)
|
|
925
|
+
vout, _ = model(vx, labels=vy)
|
|
926
|
+
tot += float(vout.loss.item()); n += 1
|
|
927
|
+
model.train()
|
|
928
|
+
return tot / max(n, 1)
|
|
929
|
+
|
|
930
|
+
if mode in {"sft", "dpo", "orpo", "grpo", "distill"}:
|
|
931
|
+
with _trainer_logger_sink(self._put):
|
|
932
|
+
if mode == "sft":
|
|
933
|
+
from chronos.trainer.sft_trainer import ChronosSFTTrainer
|
|
934
|
+
|
|
935
|
+
trainer = ChronosSFTTrainer(model, model_cfg, stage_args, tokenizer)
|
|
936
|
+
if float(getattr(model_cfg, "lambda_router_anchor", 0.0)) > 0 and loader is not None:
|
|
937
|
+
first_batch = next(iter(loader))
|
|
938
|
+
trainer.set_calibration_batch(first_batch[0])
|
|
939
|
+
self._put("[SFT] Router anchor reference captured.")
|
|
940
|
+
iters = len(loader) if stage_args.steps is None else min(stage_args.steps, len(loader))
|
|
941
|
+
self.total_steps = max(1, stage_args.epochs * iters)
|
|
942
|
+
for epoch in range(stage_args.epochs):
|
|
943
|
+
epoch_start = time.monotonic()
|
|
944
|
+
for step_idx, (ids, labels) in enumerate(loader, start=1):
|
|
945
|
+
if self._stop.is_set():
|
|
946
|
+
break
|
|
947
|
+
if stage_args.steps is not None and step_idx > stage_args.steps:
|
|
948
|
+
break
|
|
949
|
+
result = trainer.train_step(ids, labels, epoch * iters + step_idx, self.total_steps)
|
|
950
|
+
if step_idx % stage_args.log_interval == 0 or step_idx == iters:
|
|
951
|
+
elapsed = max(time.monotonic() - epoch_start, 1e-6)
|
|
952
|
+
self._record_stage_metric(mode, epoch * iters + step_idx, result, tps=step_idx / elapsed)
|
|
953
|
+
if step_idx % stage_args.save_interval == 0:
|
|
954
|
+
trainer._save(epoch, step_idx)
|
|
955
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
956
|
+
if self._stop.is_set() or stage_args.steps is not None:
|
|
957
|
+
break
|
|
958
|
+
trainer._save(epoch=stage_args.epochs - 1, step=iters)
|
|
959
|
+
elif mode == "dpo":
|
|
960
|
+
from chronos.trainer.dpo_trainer import ChronosDPOTrainer
|
|
961
|
+
|
|
962
|
+
trainer = ChronosDPOTrainer(model, model_cfg, stage_args, tokenizer)
|
|
963
|
+
if float(getattr(model_cfg, "lambda_router_anchor", 0.0)) > 0 and loader is not None:
|
|
964
|
+
first = next(iter(loader))
|
|
965
|
+
calib = torch.cat([first["x_chosen"], first["x_rejected"]], dim=0)
|
|
966
|
+
trainer.set_calibration_batch(calib)
|
|
967
|
+
self._put("[DPO] Router anchor reference captured.")
|
|
968
|
+
iters = len(loader) if stage_args.steps is None else min(stage_args.steps, len(loader))
|
|
969
|
+
self.total_steps = max(1, stage_args.epochs * iters)
|
|
970
|
+
for epoch in range(stage_args.epochs):
|
|
971
|
+
epoch_start = time.monotonic()
|
|
972
|
+
for step_idx, batch in enumerate(loader, start=1):
|
|
973
|
+
if self._stop.is_set():
|
|
974
|
+
break
|
|
975
|
+
if stage_args.steps is not None and step_idx > stage_args.steps:
|
|
976
|
+
break
|
|
977
|
+
result = trainer.train_step(batch, epoch * iters + step_idx, self.total_steps)
|
|
978
|
+
if step_idx % stage_args.log_interval == 0 or step_idx == iters:
|
|
979
|
+
elapsed = max(time.monotonic() - epoch_start, 1e-6)
|
|
980
|
+
self._record_stage_metric(mode, epoch * iters + step_idx, result, tps=step_idx / elapsed)
|
|
981
|
+
if step_idx % stage_args.save_interval == 0:
|
|
982
|
+
trainer._save(epoch, step_idx)
|
|
983
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
984
|
+
if self._stop.is_set() or stage_args.steps is not None:
|
|
985
|
+
break
|
|
986
|
+
trainer._save(epoch=stage_args.epochs - 1, step=iters)
|
|
987
|
+
elif mode == "orpo":
|
|
988
|
+
from chronos.trainer.orpo_trainer import ChronosORPOTrainer
|
|
989
|
+
|
|
990
|
+
trainer = ChronosORPOTrainer(model, model_cfg, stage_args, tokenizer)
|
|
991
|
+
if float(getattr(model_cfg, "lambda_router_anchor", 0.0)) > 0 and loader is not None:
|
|
992
|
+
first = next(iter(loader))
|
|
993
|
+
calib = torch.cat([first["x_chosen"], first["x_rejected"]], dim=0)
|
|
994
|
+
trainer.set_calibration_batch(calib)
|
|
995
|
+
self._put("[ORPO] Router anchor reference captured.")
|
|
996
|
+
iters = len(loader) if stage_args.steps is None else min(stage_args.steps, len(loader))
|
|
997
|
+
self.total_steps = max(1, stage_args.epochs * iters)
|
|
998
|
+
for epoch in range(stage_args.epochs):
|
|
999
|
+
epoch_start = time.monotonic()
|
|
1000
|
+
for step_idx, batch in enumerate(loader, start=1):
|
|
1001
|
+
if self._stop.is_set():
|
|
1002
|
+
break
|
|
1003
|
+
if stage_args.steps is not None and step_idx > stage_args.steps:
|
|
1004
|
+
break
|
|
1005
|
+
result = trainer.train_step(batch, epoch * iters + step_idx, self.total_steps)
|
|
1006
|
+
if step_idx % stage_args.log_interval == 0 or step_idx == iters:
|
|
1007
|
+
elapsed = max(time.monotonic() - epoch_start, 1e-6)
|
|
1008
|
+
self._record_stage_metric(mode, epoch * iters + step_idx, result, tps=step_idx / elapsed)
|
|
1009
|
+
if step_idx % stage_args.save_interval == 0:
|
|
1010
|
+
trainer._save(epoch, step_idx)
|
|
1011
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
1012
|
+
if self._stop.is_set() or stage_args.steps is not None:
|
|
1013
|
+
break
|
|
1014
|
+
trainer._save(epoch=stage_args.epochs - 1, step=iters)
|
|
1015
|
+
elif mode == "grpo":
|
|
1016
|
+
from chronos.trainer.grpo_trainer import ChronosGRPOTrainer
|
|
1017
|
+
from chronos.trainer.reward import build_reward_fn
|
|
1018
|
+
|
|
1019
|
+
reward_fn = build_reward_fn(stage_args.reward)
|
|
1020
|
+
trainer = ChronosGRPOTrainer(model, model_cfg, stage_args, tokenizer, reward_fn=reward_fn)
|
|
1021
|
+
if float(getattr(model_cfg, "lambda_router_anchor", 0.0)) > 0 and prompts:
|
|
1022
|
+
calib = tokenizer(
|
|
1023
|
+
prompts[0], return_tensors="pt", truncation=True, max_length=stage_args.max_seq_len
|
|
1024
|
+
).input_ids
|
|
1025
|
+
trainer.set_calibration_batch(calib)
|
|
1026
|
+
self._put("[GRPO] Router anchor reference captured.")
|
|
1027
|
+
iters = len(prompts) if stage_args.steps is None else min(stage_args.steps, len(prompts))
|
|
1028
|
+
self.total_steps = max(1, stage_args.epochs * iters)
|
|
1029
|
+
for epoch in range(stage_args.epochs):
|
|
1030
|
+
epoch_start = time.monotonic()
|
|
1031
|
+
for step_idx, prompt in enumerate(prompts, start=1):
|
|
1032
|
+
if self._stop.is_set():
|
|
1033
|
+
break
|
|
1034
|
+
if step_idx > iters:
|
|
1035
|
+
break
|
|
1036
|
+
result = trainer.train_step(prompt, epoch * iters + step_idx, self.total_steps)
|
|
1037
|
+
if step_idx % stage_args.log_interval == 0 or step_idx == iters:
|
|
1038
|
+
elapsed = max(time.monotonic() - epoch_start, 1e-6)
|
|
1039
|
+
self._record_stage_metric(mode, epoch * iters + step_idx, result, tps=step_idx / elapsed)
|
|
1040
|
+
if step_idx % stage_args.save_interval == 0:
|
|
1041
|
+
trainer._save(epoch, step_idx)
|
|
1042
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
1043
|
+
if self._stop.is_set() or stage_args.steps is not None:
|
|
1044
|
+
break
|
|
1045
|
+
trainer._save(epoch=stage_args.epochs - 1, step=iters)
|
|
1046
|
+
elif mode == "distill":
|
|
1047
|
+
from chronos.trainer.distill_trainer import ChronosDistillTrainer
|
|
1048
|
+
|
|
1049
|
+
teacher_path = stage_args.teacher_path
|
|
1050
|
+
if not os.path.exists(teacher_path):
|
|
1051
|
+
raise FileNotFoundError(f"[DISTILL] teacher not found: {teacher_path}")
|
|
1052
|
+
teacher = ChronosForCausalLM(model_cfg).to(device)
|
|
1053
|
+
t_state = torch.load(teacher_path, map_location=device)
|
|
1054
|
+
teacher.load_state_dict(t_state, strict=False)
|
|
1055
|
+
teacher.eval().requires_grad_(False)
|
|
1056
|
+
self._put(f"[DISTILL] Teacher: {teacher_path}")
|
|
1057
|
+
trainer = ChronosDistillTrainer(model, teacher, model_cfg, stage_args, tokenizer)
|
|
1058
|
+
if float(getattr(model_cfg, "lambda_router_anchor", 0.0)) > 0 and loader is not None:
|
|
1059
|
+
first = next(iter(loader))
|
|
1060
|
+
trainer.set_calibration_batch(first[0])
|
|
1061
|
+
self._put("[DISTILL] Router anchor reference captured.")
|
|
1062
|
+
iters = len(loader) if stage_args.steps is None else min(stage_args.steps, len(loader))
|
|
1063
|
+
self.total_steps = max(1, stage_args.epochs * iters)
|
|
1064
|
+
for epoch in range(stage_args.epochs):
|
|
1065
|
+
epoch_start = time.monotonic()
|
|
1066
|
+
for step_idx, (ids, labels) in enumerate(loader, start=1):
|
|
1067
|
+
if self._stop.is_set():
|
|
1068
|
+
break
|
|
1069
|
+
if stage_args.steps is not None and step_idx > stage_args.steps:
|
|
1070
|
+
break
|
|
1071
|
+
result = trainer.train_step(ids, labels, epoch * iters + step_idx, self.total_steps)
|
|
1072
|
+
if step_idx % stage_args.log_interval == 0 or step_idx == iters:
|
|
1073
|
+
elapsed = max(time.monotonic() - epoch_start, 1e-6)
|
|
1074
|
+
self._record_stage_metric(mode, epoch * iters + step_idx, result, tps=step_idx / elapsed)
|
|
1075
|
+
if step_idx % stage_args.save_interval == 0:
|
|
1076
|
+
trainer._save(epoch, step_idx)
|
|
1077
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
1078
|
+
if self._stop.is_set() or stage_args.steps is not None:
|
|
1079
|
+
break
|
|
1080
|
+
trainer._save(epoch=stage_args.epochs - 1, step=iters)
|
|
1081
|
+
self._generate_live_sample(model, device, cfg, mode)
|
|
1082
|
+
elapsed = time.monotonic() - t_start
|
|
1083
|
+
self._put(f"[{mode.upper()}] Training complete in {elapsed:.1f}s. Saved → {ckp_path}")
|
|
1084
|
+
self.status = "finished"
|
|
1085
|
+
return
|
|
1086
|
+
|
|
1087
|
+
stop_outer = False
|
|
1088
|
+
for epoch in range(epochs):
|
|
1089
|
+
if self._stop.is_set() or stop_outer:
|
|
1090
|
+
break
|
|
1091
|
+
for step, (input_ids, labels) in enumerate(loader, start=1):
|
|
1092
|
+
if self._stop.is_set():
|
|
1093
|
+
break
|
|
1094
|
+
if max_steps and global_step >= max_steps:
|
|
1095
|
+
stop_outer = True
|
|
1096
|
+
break
|
|
1097
|
+
input_ids = input_ids.to(device)
|
|
1098
|
+
labels = labels.to(device)
|
|
1099
|
+
|
|
1100
|
+
out, lp = model(input_ids, labels=labels)
|
|
1101
|
+
moe_layers = [l.mlp for l in model.model.layers
|
|
1102
|
+
if isinstance(l.mlp, ChronosMOEFeedForward)]
|
|
1103
|
+
|
|
1104
|
+
aux_val = out.aux_loss.item() if hasattr(out, "aux_loss") else 0.0
|
|
1105
|
+
|
|
1106
|
+
# Use the shared loss helper so λ_temporal / λ_lookahead
|
|
1107
|
+
# actually flow gradient back to the gate. The previous
|
|
1108
|
+
# path passed the detached `last_router_probs` into
|
|
1109
|
+
# total_loss, which made the regularizers inert (the
|
|
1110
|
+
# gate received only CE gradient, so router-locality was
|
|
1111
|
+
# never learned even after thousands of steps).
|
|
1112
|
+
from chronos.trainer.loss_mixin import (
|
|
1113
|
+
chronos_loss_term, collect_router_probs,
|
|
1114
|
+
)
|
|
1115
|
+
loss = chronos_loss_term(
|
|
1116
|
+
model, out.loss, lp, model_cfg, aux_loss=out.aux_loss,
|
|
1117
|
+
)
|
|
1118
|
+
# Logging-only: detached probs are fine here.
|
|
1119
|
+
router_4d_det = collect_router_probs(model, with_grad=False)
|
|
1120
|
+
if router_4d_det is not None and router_4d_det.shape[1] > 1:
|
|
1121
|
+
from chronos.model.temporal_loss import temporal_locality_loss
|
|
1122
|
+
temp_val = temporal_locality_loss(
|
|
1123
|
+
router_4d_det.mean(dim=2)
|
|
1124
|
+
).item()
|
|
1125
|
+
else:
|
|
1126
|
+
temp_val = 0.0
|
|
1127
|
+
|
|
1128
|
+
(loss / accum).backward()
|
|
1129
|
+
global_step += 1
|
|
1130
|
+
self.step = global_step
|
|
1131
|
+
self.loss = loss.item()
|
|
1132
|
+
|
|
1133
|
+
# LR schedule (warmup → cosine). Refresh every step.
|
|
1134
|
+
sched_total = self.total_steps or planned or (global_step + 1)
|
|
1135
|
+
apply_lr(optimizer, get_lr(global_step, sched_total, base_lr))
|
|
1136
|
+
|
|
1137
|
+
# Optimizer.step() once per `accum` micro-batches.
|
|
1138
|
+
# Off-by-one fix: trigger when (global_step % accum == 0).
|
|
1139
|
+
# Previous code did the same check but had no warmup-aware
|
|
1140
|
+
# accumulation count, occasionally stepping after step 1.
|
|
1141
|
+
if global_step % accum == 0:
|
|
1142
|
+
import torch.nn as nn
|
|
1143
|
+
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
1144
|
+
optimizer.step()
|
|
1145
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1146
|
+
|
|
1147
|
+
if global_step % log_interval == 0:
|
|
1148
|
+
now = time.monotonic()
|
|
1149
|
+
tps = log_interval / max(now - step_t, 1e-6)
|
|
1150
|
+
step_t = now
|
|
1151
|
+
ce_val = out.loss.item()
|
|
1152
|
+
tot_val = loss.item()
|
|
1153
|
+
|
|
1154
|
+
self._put(
|
|
1155
|
+
f"Epoch {epoch+1}/{epochs} Step {global_step} "
|
|
1156
|
+
f"loss={tot_val:.4f} ce={ce_val:.4f} "
|
|
1157
|
+
f"aux={aux_val:.4f} temporal={temp_val:.4f} "
|
|
1158
|
+
f"steps/s={tps:.2f} lr={optimizer.param_groups[0]['lr']:.2e}"
|
|
1159
|
+
)
|
|
1160
|
+
self._put_metric({
|
|
1161
|
+
"step": global_step,
|
|
1162
|
+
"total": tot_val,
|
|
1163
|
+
"ce": ce_val,
|
|
1164
|
+
"aux": aux_val,
|
|
1165
|
+
"temporal": temp_val,
|
|
1166
|
+
"tps": tps,
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
if global_step % save_interval == 0:
|
|
1170
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
1171
|
+
torch.save(
|
|
1172
|
+
{k: v.half().cpu() for k, v in model.state_dict().items()},
|
|
1173
|
+
ckp_path
|
|
1174
|
+
)
|
|
1175
|
+
self._put(f"Saved checkpoint → {ckp_path}")
|
|
1176
|
+
|
|
1177
|
+
# Validation eval + best-ckpt tracking.
|
|
1178
|
+
v = _eval_val()
|
|
1179
|
+
if v == v: # not NaN
|
|
1180
|
+
self._put(f"Validation loss: {v:.4f} (best={best_val:.4f})")
|
|
1181
|
+
self._put_metric({
|
|
1182
|
+
"step": global_step, "total": v, "ce": v,
|
|
1183
|
+
"aux": 0.0, "temporal": 0.0, "tps": 0.0,
|
|
1184
|
+
"val": True,
|
|
1185
|
+
"val_loss": v,
|
|
1186
|
+
})
|
|
1187
|
+
if v < best_val:
|
|
1188
|
+
best_val = v
|
|
1189
|
+
torch.save(
|
|
1190
|
+
{k: vv.half().cpu() for k, vv in model.state_dict().items()},
|
|
1191
|
+
best_path,
|
|
1192
|
+
)
|
|
1193
|
+
self._put(f" ↑ new best → {best_path}")
|
|
1194
|
+
|
|
1195
|
+
# M8a: live qualitative sample so the user sees
|
|
1196
|
+
# whether the model is actually learning, not just
|
|
1197
|
+
# whether the loss number went down. We greedy-decode
|
|
1198
|
+
# ~80 tokens from the user-supplied prompt, and fall
|
|
1199
|
+
# back to a stage-aware evaluation prompt if blank.
|
|
1200
|
+
try:
|
|
1201
|
+
sp = (cfg.get("sample_prompt") or "").strip()
|
|
1202
|
+
if not sp:
|
|
1203
|
+
sp = _stage_sample_prompt(mode)
|
|
1204
|
+
tokenizer = self._load_tokenizer()
|
|
1205
|
+
ids = tokenizer.encode(sp, return_tensors="pt").to(device)
|
|
1206
|
+
model.eval()
|
|
1207
|
+
with torch.no_grad():
|
|
1208
|
+
gen = ids.clone()
|
|
1209
|
+
for _ in range(80):
|
|
1210
|
+
o, _ = model(gen, use_cache=False)
|
|
1211
|
+
nxt = o.logits[:, -1, :].argmax(dim=-1, keepdim=True)
|
|
1212
|
+
gen = torch.cat([gen, nxt], dim=-1)
|
|
1213
|
+
if int(nxt.item()) == (tokenizer.eos_token_id or -1):
|
|
1214
|
+
break
|
|
1215
|
+
txt = tokenizer.decode(gen[0].tolist(), skip_special_tokens=True)
|
|
1216
|
+
model.train()
|
|
1217
|
+
self.last_sample = txt
|
|
1218
|
+
self.last_sample_step = global_step
|
|
1219
|
+
self._put(f"[sample @ step {global_step}] {txt[:200]}")
|
|
1220
|
+
except Exception as gen_err:
|
|
1221
|
+
self._put(f"[sample failed: {gen_err}]")
|
|
1222
|
+
|
|
1223
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
1224
|
+
torch.save(
|
|
1225
|
+
{k: v.half().cpu() for k, v in model.state_dict().items()},
|
|
1226
|
+
ckp_path
|
|
1227
|
+
)
|
|
1228
|
+
elapsed = time.monotonic() - t_start
|
|
1229
|
+
self._put(f"Training complete in {elapsed:.1f}s. Saved → {ckp_path}")
|
|
1230
|
+
self.status = "finished"
|
|
1231
|
+
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
import traceback
|
|
1234
|
+
self._put(f"[ERROR] {e}\n{traceback.format_exc()}")
|
|
1235
|
+
self.status = "error"
|
|
1236
|
+
|
|
1237
|
+
def _synthetic_loader(self, vocab_size, seq_len, batch_size, n=50):
|
|
1238
|
+
import torch
|
|
1239
|
+
from torch.utils.data import TensorDataset, DataLoader
|
|
1240
|
+
ids = torch.randint(0, vocab_size, (n * batch_size, seq_len))
|
|
1241
|
+
ds = TensorDataset(ids, ids.clone())
|
|
1242
|
+
return DataLoader(ds, batch_size=batch_size)
|
|
1243
|
+
|
|
1244
|
+
def _load_tokenizer(self):
|
|
1245
|
+
from transformers import AutoTokenizer
|
|
1246
|
+
return AutoTokenizer.from_pretrained(chronos.deps.get_tokenizer_path())
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
_session = TrainSession()
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _fmt_eta(seconds: float) -> str:
|
|
1253
|
+
if seconds is None or seconds < 0 or seconds != seconds:
|
|
1254
|
+
return "—"
|
|
1255
|
+
if seconds < 60:
|
|
1256
|
+
return f"{seconds:.0f}s"
|
|
1257
|
+
if seconds < 3600:
|
|
1258
|
+
return f"{seconds/60:.1f}min"
|
|
1259
|
+
return f"{seconds/3600:.1f}h"
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def _make_loss_chart(metrics: list[dict], mode: str = "pretrain", total_steps: int = 0, t_start: float = 0.0):
|
|
1263
|
+
"""Build matplotlib figure: loss curves + throughput + progress/ETA."""
|
|
1264
|
+
try:
|
|
1265
|
+
import matplotlib
|
|
1266
|
+
matplotlib.use("Agg")
|
|
1267
|
+
import matplotlib.pyplot as plt
|
|
1268
|
+
meta = STAGE_CHART_META.get(mode, STAGE_CHART_META["pretrain"])
|
|
1269
|
+
|
|
1270
|
+
# Close any prior figures held by this thread to avoid the
|
|
1271
|
+
# "More than 20 figures opened" warning Gradio's Plot component triggers.
|
|
1272
|
+
plt.close("all")
|
|
1273
|
+
|
|
1274
|
+
if not metrics:
|
|
1275
|
+
fig, ax = plt.subplots(figsize=(10, 3))
|
|
1276
|
+
ax.text(0.5, 0.5, "No data yet", ha="center", va="center",
|
|
1277
|
+
transform=ax.transAxes, fontsize=13, color="#888")
|
|
1278
|
+
ax.set_axis_off()
|
|
1279
|
+
plt.tight_layout()
|
|
1280
|
+
return fig
|
|
1281
|
+
|
|
1282
|
+
steps = [m["step"] for m in metrics]
|
|
1283
|
+
cur_step = steps[-1]
|
|
1284
|
+
elapsed = max(0.0, time.monotonic() - t_start) if t_start else 0.0
|
|
1285
|
+
# ETA from average step rate so far (not last point — smoother).
|
|
1286
|
+
rate = cur_step / elapsed if elapsed > 0 else 0.0
|
|
1287
|
+
remaining = (total_steps - cur_step) / rate if rate > 0 and total_steps else float("nan")
|
|
1288
|
+
progress_pct = min(100.0, 100.0 * cur_step / total_steps) if total_steps else 0.0
|
|
1289
|
+
|
|
1290
|
+
# M8b: split val markers from train metrics.
|
|
1291
|
+
train_metrics = [m for m in metrics if not m.get("val")]
|
|
1292
|
+
val_metrics = [m for m in metrics if m.get("val")]
|
|
1293
|
+
train_steps = [m["step"] for m in train_metrics]
|
|
1294
|
+
val_steps = [m["step"] for m in val_metrics]
|
|
1295
|
+
|
|
1296
|
+
fig, axes = plt.subplots(1, 3, figsize=(13, 3.5),
|
|
1297
|
+
gridspec_kw={"width_ratios": [4, 3, 3]})
|
|
1298
|
+
fig.patch.set_facecolor("#1a1a2e")
|
|
1299
|
+
|
|
1300
|
+
# ── Left: loss curves
|
|
1301
|
+
ax = axes[0]
|
|
1302
|
+
ax.set_facecolor("#16213e")
|
|
1303
|
+
if train_metrics:
|
|
1304
|
+
ax.plot(train_steps, [m["total"] for m in train_metrics], color="#e94560", lw=1.5, label="train total")
|
|
1305
|
+
ax.plot(train_steps, [m.get("primary", m.get("ce", 0.0)) for m in train_metrics],
|
|
1306
|
+
color="#0f3460", lw=1.2, label=meta["primary"])
|
|
1307
|
+
ax.plot(train_steps, [m.get("secondary", m.get("aux", 0.0)) for m in train_metrics],
|
|
1308
|
+
color="#533483", lw=1.0, label=meta["secondary"])
|
|
1309
|
+
ax.plot(train_steps, [m.get("tertiary", m.get("temporal", 0.0)) for m in train_metrics],
|
|
1310
|
+
color="#e2b714", lw=0.9, label=meta["tertiary"], ls="--")
|
|
1311
|
+
if val_metrics:
|
|
1312
|
+
ax.plot(val_steps, [m.get("primary", m.get("ce", 0.0)) for m in val_metrics], color="#39d98a",
|
|
1313
|
+
lw=2.0, marker="o", markersize=4, label=meta["val"])
|
|
1314
|
+
ax.set_title(meta["title"], color="white", fontsize=11)
|
|
1315
|
+
ax.set_xlabel("Step", color="#aaa")
|
|
1316
|
+
ax.tick_params(colors="#aaa")
|
|
1317
|
+
for spine in ax.spines.values():
|
|
1318
|
+
spine.set_edgecolor("#333")
|
|
1319
|
+
ax.legend(fontsize=8, facecolor="#16213e", labelcolor="white")
|
|
1320
|
+
|
|
1321
|
+
# ── Middle: stage-aware speed panel
|
|
1322
|
+
ax2 = axes[1]
|
|
1323
|
+
ax2.set_facecolor("#16213e")
|
|
1324
|
+
tps_steps = [m["step"] for m in train_metrics if m.get("tps", 0) > 0]
|
|
1325
|
+
tps_vals = [m["tps"] for m in train_metrics if m.get("tps", 0) > 0]
|
|
1326
|
+
overlay_ax = None
|
|
1327
|
+
if mode == "grpo":
|
|
1328
|
+
reward_steps = [m["step"] for m in train_metrics if "tertiary" in m]
|
|
1329
|
+
reward_vals = [m.get("tertiary", 0.0) for m in train_metrics if "tertiary" in m]
|
|
1330
|
+
if tps_steps:
|
|
1331
|
+
ax2.plot(
|
|
1332
|
+
tps_steps,
|
|
1333
|
+
tps_vals,
|
|
1334
|
+
color="#00b4d8",
|
|
1335
|
+
lw=1.6,
|
|
1336
|
+
label=meta.get("speed_label", "steps/s"),
|
|
1337
|
+
)
|
|
1338
|
+
ax2.fill_between(tps_steps, tps_vals, alpha=0.18, color="#00b4d8")
|
|
1339
|
+
if reward_steps:
|
|
1340
|
+
overlay_ax = ax2.twinx()
|
|
1341
|
+
overlay_ax.set_facecolor("none")
|
|
1342
|
+
overlay_ax.plot(
|
|
1343
|
+
reward_steps,
|
|
1344
|
+
reward_vals,
|
|
1345
|
+
color="#e2b714",
|
|
1346
|
+
lw=1.3,
|
|
1347
|
+
ls="--",
|
|
1348
|
+
label=meta.get("speed_overlay", meta["tertiary"]),
|
|
1349
|
+
)
|
|
1350
|
+
overlay_ax.tick_params(colors="#f1d36b")
|
|
1351
|
+
overlay_ax.set_ylabel(meta.get("speed_overlay", meta["tertiary"]), color="#f1d36b", fontsize=9)
|
|
1352
|
+
for spine in overlay_ax.spines.values():
|
|
1353
|
+
spine.set_edgecolor("#333")
|
|
1354
|
+
elif tps_steps:
|
|
1355
|
+
ax2.plot(
|
|
1356
|
+
tps_steps,
|
|
1357
|
+
tps_vals,
|
|
1358
|
+
color="#00b4d8",
|
|
1359
|
+
lw=1.5,
|
|
1360
|
+
label=meta.get("speed_label", "steps/s"),
|
|
1361
|
+
)
|
|
1362
|
+
ax2.fill_between(tps_steps, tps_vals, alpha=0.2, color="#00b4d8")
|
|
1363
|
+
ax2.set_title(meta.get("speed_title", "Training Speed (steps/s)"), color="white", fontsize=11)
|
|
1364
|
+
ax2.set_xlabel("Step", color="#aaa")
|
|
1365
|
+
ax2.set_ylabel(meta.get("speed_label", "steps/s"), color="#8ae9ff", fontsize=9)
|
|
1366
|
+
ax2.tick_params(colors="#aaa")
|
|
1367
|
+
for spine in ax2.spines.values():
|
|
1368
|
+
spine.set_edgecolor("#333")
|
|
1369
|
+
if tps_steps or overlay_ax is not None:
|
|
1370
|
+
lines, labels = ax2.get_legend_handles_labels()
|
|
1371
|
+
if overlay_ax is not None:
|
|
1372
|
+
extra_lines, extra_labels = overlay_ax.get_legend_handles_labels()
|
|
1373
|
+
lines += extra_lines
|
|
1374
|
+
labels += extra_labels
|
|
1375
|
+
if labels:
|
|
1376
|
+
ax2.legend(lines, labels, fontsize=8, facecolor="#16213e", labelcolor="white", loc="upper right")
|
|
1377
|
+
|
|
1378
|
+
# ── Right: progress bar + numeric panel
|
|
1379
|
+
ax3 = axes[2]
|
|
1380
|
+
ax3.set_facecolor("#16213e")
|
|
1381
|
+
# Horizontal bar
|
|
1382
|
+
bar_y = 0.65
|
|
1383
|
+
ax3.barh([bar_y], [100], height=0.18, color="#2a2a44", edgecolor="#444")
|
|
1384
|
+
ax3.barh([bar_y], [progress_pct], height=0.18, color="#39d98a")
|
|
1385
|
+
ax3.set_xlim(0, 100); ax3.set_ylim(0, 1)
|
|
1386
|
+
ax3.set_xticks([0, 25, 50, 75, 100])
|
|
1387
|
+
ax3.set_yticks([])
|
|
1388
|
+
ax3.tick_params(colors="#aaa")
|
|
1389
|
+
for spine in ax3.spines.values():
|
|
1390
|
+
spine.set_edgecolor("#333")
|
|
1391
|
+
|
|
1392
|
+
# Big text overlay
|
|
1393
|
+
ax3.text(50, 0.92, f"Progress {progress_pct:5.1f}%",
|
|
1394
|
+
ha="center", va="center", color="white", fontsize=12, weight="bold")
|
|
1395
|
+
eta_str = _fmt_eta(remaining)
|
|
1396
|
+
elapsed_str = _fmt_eta(elapsed)
|
|
1397
|
+
ax3.text(50, 0.42, f"step {cur_step:,} / {total_steps or '?':,}",
|
|
1398
|
+
ha="center", va="center", color="#cfd8e3", fontsize=11)
|
|
1399
|
+
ax3.text(50, 0.25, f"elapsed {elapsed_str} ETA {eta_str}",
|
|
1400
|
+
ha="center", va="center", color="#9aa6b2", fontsize=10)
|
|
1401
|
+
ax3.text(50, 0.10, f"~{rate:.2f} {meta.get('speed_label', 'steps/s')} avg",
|
|
1402
|
+
ha="center", va="center", color="#9aa6b2", fontsize=9)
|
|
1403
|
+
ax3.set_title("Progress & ETA", color="white", fontsize=11)
|
|
1404
|
+
|
|
1405
|
+
plt.tight_layout(pad=1.5)
|
|
1406
|
+
return fig
|
|
1407
|
+
|
|
1408
|
+
except Exception:
|
|
1409
|
+
return None
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def build_train_tab(config_state: gr.State, cfg_save_dir: gr.Textbox):
|
|
1413
|
+
with gr.Tab(t("tab.train")) as tab:
|
|
1414
|
+
register_translatable(tab, "tab.train")
|
|
1415
|
+
|
|
1416
|
+
# ── Train owns its dataset choice (moved out of Config tab) ─
|
|
1417
|
+
stage_help = gr.Markdown(_stage_text(STAGE_HELP_TEXT, "pretrain"))
|
|
1418
|
+
with gr.Row():
|
|
1419
|
+
data_path = gr.Textbox(
|
|
1420
|
+
label=t("config.data_path"),
|
|
1421
|
+
value=STAGE_DEFAULT_DATA["pretrain"],
|
|
1422
|
+
placeholder=STAGE_DEFAULT_DATA["pretrain"],
|
|
1423
|
+
scale=3,
|
|
1424
|
+
)
|
|
1425
|
+
register_translatable(data_path, "config.data_path")
|
|
1426
|
+
dataset_upload = gr.File(
|
|
1427
|
+
label=t("train.dataset_upload"),
|
|
1428
|
+
file_types=[".jsonl"], type="filepath", scale=2,
|
|
1429
|
+
)
|
|
1430
|
+
register_translatable(dataset_upload, "train.dataset_upload")
|
|
1431
|
+
dataset_upload.change(
|
|
1432
|
+
fn=lambda fp: fp or "",
|
|
1433
|
+
inputs=dataset_upload,
|
|
1434
|
+
outputs=data_path,
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
# ── Mode + controls ───────────────────────────────────────
|
|
1438
|
+
with gr.Row():
|
|
1439
|
+
mode = gr.Radio(
|
|
1440
|
+
STAGE_UI_ORDER,
|
|
1441
|
+
value="pretrain", label=t("train.mode")
|
|
1442
|
+
)
|
|
1443
|
+
register_translatable(mode, "train.mode")
|
|
1444
|
+
train_backend = gr.Dropdown(
|
|
1445
|
+
choices=_train_backend_dropdown_choices(),
|
|
1446
|
+
value=_default_train_backend_value(),
|
|
1447
|
+
label=t("train.backend"),
|
|
1448
|
+
scale=1,
|
|
1449
|
+
)
|
|
1450
|
+
register_translatable(train_backend, "train.backend")
|
|
1451
|
+
status_box = gr.Textbox(
|
|
1452
|
+
value="idle", label=t("train.status"),
|
|
1453
|
+
interactive=False, scale=1,
|
|
1454
|
+
)
|
|
1455
|
+
register_translatable(status_box, "train.status")
|
|
1456
|
+
|
|
1457
|
+
# ── Init-weight / stage-specific extras ────────────────────
|
|
1458
|
+
with gr.Row():
|
|
1459
|
+
init_weight = gr.Textbox(
|
|
1460
|
+
label=t("train.init_weight"),
|
|
1461
|
+
placeholder=_stage_init_placeholder("pretrain"),
|
|
1462
|
+
scale=3,
|
|
1463
|
+
)
|
|
1464
|
+
register_translatable(init_weight, "train.init_weight")
|
|
1465
|
+
teacher_path = gr.Textbox(
|
|
1466
|
+
label=t("pipeline.teacher_path"),
|
|
1467
|
+
placeholder=_distill_teacher_placeholder(),
|
|
1468
|
+
visible=False,
|
|
1469
|
+
scale=2,
|
|
1470
|
+
)
|
|
1471
|
+
register_translatable(teacher_path, "pipeline.teacher_path")
|
|
1472
|
+
|
|
1473
|
+
with gr.Row():
|
|
1474
|
+
max_steps_in = gr.Number(
|
|
1475
|
+
value=0, precision=0,
|
|
1476
|
+
label=t("train.max_steps"), scale=1,
|
|
1477
|
+
)
|
|
1478
|
+
val_ratio_in = gr.Slider(
|
|
1479
|
+
0.0, 0.5, value=0.05, step=0.01,
|
|
1480
|
+
label=t("train.val_ratio"), scale=2,
|
|
1481
|
+
)
|
|
1482
|
+
weight_decay_in = gr.Number(
|
|
1483
|
+
value=0.01, precision=4,
|
|
1484
|
+
label=t("train.weight_decay"), scale=1,
|
|
1485
|
+
)
|
|
1486
|
+
register_translatable(max_steps_in, "train.max_steps")
|
|
1487
|
+
register_translatable(val_ratio_in, "train.val_ratio")
|
|
1488
|
+
register_translatable(weight_decay_in, "train.weight_decay")
|
|
1489
|
+
|
|
1490
|
+
with gr.Row():
|
|
1491
|
+
reward_spec_in = gr.Textbox(
|
|
1492
|
+
value="toy",
|
|
1493
|
+
label="reward",
|
|
1494
|
+
placeholder="toy or lm:/path/to/reward-model",
|
|
1495
|
+
visible=False,
|
|
1496
|
+
scale=2,
|
|
1497
|
+
)
|
|
1498
|
+
grpo_temp_in = gr.Number(
|
|
1499
|
+
value=1.0, precision=3,
|
|
1500
|
+
label="temperature",
|
|
1501
|
+
visible=False,
|
|
1502
|
+
scale=1,
|
|
1503
|
+
)
|
|
1504
|
+
grpo_num_gen_in = gr.Number(
|
|
1505
|
+
value=4, precision=0,
|
|
1506
|
+
label="num_generations",
|
|
1507
|
+
visible=False,
|
|
1508
|
+
scale=1,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
with gr.Row():
|
|
1512
|
+
sample_prompt_in = gr.Textbox(
|
|
1513
|
+
value=_stage_sample_prompt("pretrain"),
|
|
1514
|
+
label=t("train.sample_prompt"),
|
|
1515
|
+
placeholder=_stage_sample_prompt("pretrain"),
|
|
1516
|
+
scale=4,
|
|
1517
|
+
)
|
|
1518
|
+
register_translatable(sample_prompt_in, "train.sample_prompt")
|
|
1519
|
+
|
|
1520
|
+
with gr.Row():
|
|
1521
|
+
start_btn = gr.Button(t("train.start"), variant="primary")
|
|
1522
|
+
stop_btn = gr.Button(t("train.stop"), variant="stop")
|
|
1523
|
+
clear_btn = gr.Button(t("train.clear_log"), variant="secondary")
|
|
1524
|
+
register_translatable(start_btn, "train.start")
|
|
1525
|
+
register_translatable(stop_btn, "train.stop")
|
|
1526
|
+
register_translatable(clear_btn, "train.clear_log")
|
|
1527
|
+
|
|
1528
|
+
# ── Metrics chart ─────────────────────────────────────────
|
|
1529
|
+
chart = gr.Plot(label=t("train.chart"), show_label=True)
|
|
1530
|
+
register_translatable(chart, "train.chart")
|
|
1531
|
+
|
|
1532
|
+
# ── Scalar readouts ───────────────────────────────────────
|
|
1533
|
+
with gr.Row():
|
|
1534
|
+
step_box = gr.Number(label=t("train.step"), value=0, interactive=False, precision=0)
|
|
1535
|
+
loss_box = gr.Number(label=STAGE_METRIC_LABELS["pretrain"][0], value=0.0, interactive=False, precision=4)
|
|
1536
|
+
ce_box = gr.Number(label=STAGE_METRIC_LABELS["pretrain"][1], value=0.0, interactive=False, precision=4)
|
|
1537
|
+
aux_box = gr.Number(label=STAGE_METRIC_LABELS["pretrain"][2], value=0.0, interactive=False, precision=4)
|
|
1538
|
+
tps_box = gr.Number(label=t("train.tps"), value=0.0, interactive=False, precision=2)
|
|
1539
|
+
register_translatable(step_box, "train.step")
|
|
1540
|
+
register_translatable(tps_box, "train.tps")
|
|
1541
|
+
|
|
1542
|
+
# ── Scrollable log ────────────────────────────────────────
|
|
1543
|
+
log_box = gr.Textbox(
|
|
1544
|
+
label=t("train.log"), lines=18, max_lines=30,
|
|
1545
|
+
interactive=False, autoscroll=True,
|
|
1546
|
+
)
|
|
1547
|
+
register_translatable(log_box, "train.log")
|
|
1548
|
+
|
|
1549
|
+
# ── Live generation sample (updated each save_interval) ───
|
|
1550
|
+
sample_box = gr.Textbox(
|
|
1551
|
+
label=t("train.sample_output"), lines=6,
|
|
1552
|
+
interactive=False, autoscroll=False,
|
|
1553
|
+
)
|
|
1554
|
+
register_translatable(sample_box, "train.sample_output")
|
|
1555
|
+
|
|
1556
|
+
# ── Callbacks ─────────────────────────────────────────────
|
|
1557
|
+
|
|
1558
|
+
def sync_stage_fields(train_mode, current_prompt, current_data_path, current_init_weight):
|
|
1559
|
+
current = (current_prompt or "").strip()
|
|
1560
|
+
default_upstream = STAGE_DEFAULT_INIT.get(train_mode, "")
|
|
1561
|
+
help_md = _stage_text(STAGE_HELP_TEXT, train_mode)
|
|
1562
|
+
data_default = STAGE_DEFAULT_DATA.get(train_mode, "")
|
|
1563
|
+
metric_labels = STAGE_METRIC_LABELS.get(train_mode, STAGE_METRIC_LABELS["pretrain"])
|
|
1564
|
+
init_placeholder = _stage_init_placeholder(train_mode)
|
|
1565
|
+
current_data = (current_data_path or "").strip()
|
|
1566
|
+
current_init = _normalize_stage_init_value(current_init_weight)
|
|
1567
|
+
data_value = current_data
|
|
1568
|
+
if (not current_data) or current_data in DEFAULT_STAGE_DATA_VALUES:
|
|
1569
|
+
data_value = data_default
|
|
1570
|
+
init_value = current_init
|
|
1571
|
+
teacher_visible = train_mode == "distill"
|
|
1572
|
+
reward_visible = train_mode == "grpo"
|
|
1573
|
+
if (not current) or current in DEFAULT_SAMPLE_PROMPT_VALUES:
|
|
1574
|
+
prompt_update = gr.update(
|
|
1575
|
+
value=_stage_sample_prompt(train_mode),
|
|
1576
|
+
placeholder=_stage_sample_prompt(train_mode),
|
|
1577
|
+
)
|
|
1578
|
+
else:
|
|
1579
|
+
prompt_update = gr.update(placeholder=_stage_sample_prompt(train_mode))
|
|
1580
|
+
return (
|
|
1581
|
+
prompt_update,
|
|
1582
|
+
gr.update(value=help_md),
|
|
1583
|
+
gr.update(value=data_value, placeholder=data_default),
|
|
1584
|
+
gr.update(value=init_value, placeholder=init_placeholder),
|
|
1585
|
+
gr.update(visible=teacher_visible, placeholder=_distill_teacher_placeholder()),
|
|
1586
|
+
gr.update(visible=reward_visible),
|
|
1587
|
+
gr.update(visible=reward_visible),
|
|
1588
|
+
gr.update(visible=reward_visible),
|
|
1589
|
+
gr.update(label=metric_labels[0]),
|
|
1590
|
+
gr.update(label=metric_labels[1]),
|
|
1591
|
+
gr.update(label=metric_labels[2]),
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
def start_training(cfg, train_mode, selected_backend, dpath, iw, ms, vr, wd, reward_spec, grpo_temp, grpo_num_gen, teacher, sp):
|
|
1595
|
+
if _session.is_running():
|
|
1596
|
+
return "already running", 0, 0.0, 0.0, 0.0, 0.0, "Already running.\n", None, ""
|
|
1597
|
+
cfg = dict(cfg) if cfg else {}
|
|
1598
|
+
requested_backend = (selected_backend or "auto").strip().lower() or "auto"
|
|
1599
|
+
_, resolved_device = resolve_training_device(requested_backend)
|
|
1600
|
+
cfg["train_backend"] = requested_backend
|
|
1601
|
+
cfg["device"] = resolved_device
|
|
1602
|
+
cfg["data_path"] = dpath or cfg.get("data_path", "")
|
|
1603
|
+
cfg["init_weight"] = _normalize_stage_init_value(iw)
|
|
1604
|
+
cfg["max_steps"] = int(ms or 0)
|
|
1605
|
+
cfg["val_ratio"] = float(vr or 0.0)
|
|
1606
|
+
cfg["weight_decay"] = float(wd or 0.0)
|
|
1607
|
+
cfg["reward_spec"] = reward_spec or "toy"
|
|
1608
|
+
cfg["temperature"] = float(grpo_temp or 1.0)
|
|
1609
|
+
cfg["num_generations"] = int(grpo_num_gen or 4)
|
|
1610
|
+
cfg["teacher_path"] = teacher or ""
|
|
1611
|
+
cfg["sample_prompt"] = sp or ""
|
|
1612
|
+
_session.start(cfg, train_mode)
|
|
1613
|
+
return "running", 0, 0.0, 0.0, 0.0, 0.0, "Starting...\n", None, ""
|
|
1614
|
+
|
|
1615
|
+
def stop_training():
|
|
1616
|
+
_session.stop()
|
|
1617
|
+
return "stopped"
|
|
1618
|
+
|
|
1619
|
+
def clear_log():
|
|
1620
|
+
return ""
|
|
1621
|
+
|
|
1622
|
+
def poll(current_log: str, current_mode: str):
|
|
1623
|
+
new_lines = _session.drain_log()
|
|
1624
|
+
# Append new lines to existing log; keep last 500 lines to avoid unbounded growth
|
|
1625
|
+
if new_lines:
|
|
1626
|
+
combined = (current_log + "\n" + new_lines).lstrip("\n")
|
|
1627
|
+
else:
|
|
1628
|
+
combined = current_log
|
|
1629
|
+
|
|
1630
|
+
lines = combined.split("\n")
|
|
1631
|
+
if len(lines) > 500:
|
|
1632
|
+
combined = "\n".join(lines[-500:])
|
|
1633
|
+
|
|
1634
|
+
metrics = _session.get_metrics()
|
|
1635
|
+
fig = _make_loss_chart(metrics, current_mode, _session.total_steps, _session.t_start)
|
|
1636
|
+
|
|
1637
|
+
# Latest scalar values from last metric entry
|
|
1638
|
+
if metrics:
|
|
1639
|
+
last = metrics[-1]
|
|
1640
|
+
ce_v = last.get("primary", last.get("ce", 0.0))
|
|
1641
|
+
aux_v = last.get("secondary", last.get("aux", 0.0))
|
|
1642
|
+
tps_v = last.get("tps", 0.0)
|
|
1643
|
+
else:
|
|
1644
|
+
ce_v = aux_v = tps_v = 0.0
|
|
1645
|
+
|
|
1646
|
+
return (
|
|
1647
|
+
_session.status,
|
|
1648
|
+
_session.step or 0,
|
|
1649
|
+
_session.loss or 0.0,
|
|
1650
|
+
ce_v,
|
|
1651
|
+
aux_v,
|
|
1652
|
+
tps_v,
|
|
1653
|
+
combined,
|
|
1654
|
+
fig,
|
|
1655
|
+
_session.last_sample or "",
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
mode.change(
|
|
1659
|
+
fn=sync_stage_fields,
|
|
1660
|
+
inputs=[mode, sample_prompt_in, data_path, init_weight],
|
|
1661
|
+
outputs=[
|
|
1662
|
+
sample_prompt_in, stage_help, data_path, init_weight,
|
|
1663
|
+
teacher_path, reward_spec_in, grpo_temp_in, grpo_num_gen_in,
|
|
1664
|
+
loss_box, ce_box, aux_box,
|
|
1665
|
+
],
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
start_btn.click(
|
|
1669
|
+
fn=start_training,
|
|
1670
|
+
inputs=[config_state, mode, train_backend, data_path, init_weight,
|
|
1671
|
+
max_steps_in, val_ratio_in, weight_decay_in,
|
|
1672
|
+
reward_spec_in, grpo_temp_in, grpo_num_gen_in, teacher_path,
|
|
1673
|
+
sample_prompt_in],
|
|
1674
|
+
outputs=[status_box, step_box, loss_box, ce_box, aux_box, tps_box,
|
|
1675
|
+
log_box, chart, sample_box],
|
|
1676
|
+
)
|
|
1677
|
+
stop_btn.click(fn=stop_training, outputs=[status_box])
|
|
1678
|
+
clear_btn.click(fn=clear_log, outputs=[log_box])
|
|
1679
|
+
|
|
1680
|
+
timer = gr.Timer(value=2.0)
|
|
1681
|
+
timer.tick(
|
|
1682
|
+
fn=poll,
|
|
1683
|
+
inputs=[log_box, mode],
|
|
1684
|
+
outputs=[status_box, step_box, loss_box, ce_box, aux_box, tps_box,
|
|
1685
|
+
log_box, chart, sample_box],
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
return tab
|