File size: 9,142 Bytes
1672b09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81a4c44
 
1672b09
 
4079463
 
1672b09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2329aa
1672b09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
"""
spaces/train/app.py — TriChronos Training Monitor
Gradio UI that:
  • Launches train.py as a managed subprocess
  • Streams live logs to the browser
  • Shows elapsed time, budget consumption, and latest loss
  • Provides Start / Stop training buttons
  • Auto-resumes if a checkpoint exists on startup

Runs inside the Docker training Space on HF (port 7860).
"""

from __future__ import annotations

import os
import subprocess
import threading
import time
from pathlib import Path

import gradio as gr

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

WALL_CLOCK_LIMIT = 7 * 3600 + 10 * 60   # 7 h 10 m
HOURLY_COST = 1.80                        # USD/hr for 1x L40S (actual Space hardware)
TOTAL_BUDGET = 15.00                      # USD hard cap
LOG_TAIL_LINES = 120                      # lines shown in UI

CHECKPOINT_DIR = Path("checkpoints")
LOG_FILE = Path("train.log")

# ---------------------------------------------------------------------------
# Global training state (module-level, protected by _lock)
# ---------------------------------------------------------------------------

_proc: subprocess.Popen | None = None
_lock = threading.Lock()
_train_start: float | None = None
_log_file_handle = None


# ---------------------------------------------------------------------------
# Subprocess management
# ---------------------------------------------------------------------------

def _is_running() -> bool:
    with _lock:
        return _proc is not None and _proc.poll() is None


def _log_writer(proc: subprocess.Popen):
    """Thread: reads stdout from train.py, writes to log file."""
    with open(LOG_FILE, "a", buffering=1) as f:
        for line in proc.stdout:
            f.write(line)
            f.flush()


def start_training() -> tuple[str, str]:
    """
    Start train.py as a subprocess.
    Returns (status_message, button_label).
    """
    global _proc, _train_start

    if _is_running():
        return "⚠️ Training is already running.", gr.update()

    CHECKPOINT_DIR.mkdir(exist_ok=True)
    LOG_FILE.parent.mkdir(exist_ok=True)

    # Auto-resume if checkpoint exists
    resume = (CHECKPOINT_DIR / "model_state.pt").exists()
    cmd = ["python", "train.py", "--batch-size", "32"]
    if resume:
        cmd.append("--resume")

    with _lock:
        _train_start = time.time()
        _proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
            env={**os.environ},
        )

    # Background thread writes logs to file
    threading.Thread(target=_log_writer, args=(_proc,), daemon=True).start()

    mode = "Resuming from checkpoint" if resume else "Starting fresh"
    return f"✅ {mode} — PID {_proc.pid}", gr.update()


def stop_training() -> tuple[str, str]:
    """Send SIGTERM to train.py (triggers graceful checkpoint + Space pause)."""
    global _proc

    with _lock:
        proc = _proc

    if proc is None or proc.poll() is not None:
        return "ℹ️ No training process running.", gr.update()

    proc.terminate()
    try:
        proc.wait(timeout=30)
    except subprocess.TimeoutExpired:
        proc.kill()

    return "🛑 Training stopped (checkpoint saved).", gr.update()


# ---------------------------------------------------------------------------
# Status helpers
# ---------------------------------------------------------------------------

def _read_log_tail() -> str:
    if not LOG_FILE.exists():
        return "(no log yet — start training to begin)"
    try:
        with open(LOG_FILE, "r") as f:
            lines = f.readlines()
        return "".join(lines[-LOG_TAIL_LINES:])
    except Exception as exc:
        return f"(error reading log: {exc})"


def _read_latest_loss() -> str:
    loss_file = CHECKPOINT_DIR / "loss.txt"
    if loss_file.exists():
        try:
            return loss_file.read_text().strip()
        except Exception:
            pass
    return "N/A"


def _read_latest_step() -> str:
    step_file = CHECKPOINT_DIR / "step.txt"
    if step_file.exists():
        try:
            return f"{int(step_file.read_text().strip()):,}"
        except Exception:
            pass
    return "0"


def get_status() -> tuple[str, str, str, str, str, str]:
    """
    Returns:
        status_icon, status_text, elapsed_str, budget_str, loss_str, step_str
    """
    running = _is_running()
    status_icon = "🟢 Running" if running else "⚫ Idle"

    # Time + budget
    if _train_start is not None:
        elapsed_s = time.time() - _train_start
        elapsed_h = elapsed_s / 3600
        pct = min(elapsed_s / WALL_CLOCK_LIMIT * 100, 100)
        cost = elapsed_h * HOURLY_COST
        elapsed_str = f"{elapsed_h:.2f} h  ({pct:.1f}% of budget)"
        budget_str = f"${cost:.2f} spent  /  ${TOTAL_BUDGET:.2f} total"
    else:
        elapsed_str = "—"
        budget_str = f"$0.00  /  ${TOTAL_BUDGET:.2f}"

    loss_str = _read_latest_loss()
    step_str = _read_latest_step()
    log_text = _read_log_tail()

    return status_icon, elapsed_str, budget_str, loss_str, step_str, log_text


# ---------------------------------------------------------------------------
# Auto-start on Space boot (if HF_TOKEN is set, indicating a real GPU Space)
# ---------------------------------------------------------------------------

def _maybe_autostart():
    """If we're in a real HF Space (SPACE_ID set) and not already running, start training."""
    if os.environ.get("TRICHRONOS_SPACE_ID") and not _is_running():
        time.sleep(3)   # Give Gradio time to fully start
        start_training()


# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------

CUSTOM_CSS = """
#log-box textarea {
    font-family: 'Courier New', monospace;
    font-size: 12px;
    background: #0d1117;
    color: #c9d1d9;
}
.metric-card {
    background: linear-gradient(135deg, #1a1f2e, #252b3b);
    border: 1px solid #30363d;
    border-radius: 8px;
    padding: 12px;
    text-align: center;
}
"""

with gr.Blocks(
    title="TriChronos Training Monitor",
    theme=gr.themes.Base(
        primary_hue=gr.themes.colors.indigo,
        neutral_hue=gr.themes.colors.slate,
    ),
    css=CUSTOM_CSS,
) as demo:

    gr.Markdown("""
# 🧠 TriChronos-0.1B — Training Monitor
**100M-parameter ternary-quantised time-series forecasting model**  
Dataset: [Salesforce/lotsa_data](https://huggingface.co/datasets/Salesforce/lotsa_data) · 
Model: [iravikr/trichronos-0.1b](https://huggingface.co/iravikr/trichronos-0.1b)
""")

    with gr.Row():
        with gr.Column(scale=1):
            status_icon = gr.Textbox(
                label="Status", value="⚫ Idle", interactive=False, elem_id="status"
            )
        with gr.Column(scale=2):
            elapsed_box = gr.Textbox(label="Elapsed / Budget %", value="—", interactive=False)
        with gr.Column(scale=2):
            budget_box = gr.Textbox(label="Cost", value="$0.00 / $18.00", interactive=False)
        with gr.Column(scale=1):
            loss_box = gr.Textbox(label="Latest Loss", value="N/A", interactive=False)
        with gr.Column(scale=1):
            step_box = gr.Textbox(label="Step", value="0", interactive=False)

    with gr.Row():
        start_btn = gr.Button("▶ Start Training", variant="primary", size="lg")
        stop_btn = gr.Button("⏹ Stop Training", variant="stop", size="lg")

    msg_box = gr.Textbox(label="Last action", value="", interactive=False)

    gr.Markdown("### Live Training Log")
    log_box = gr.Textbox(
        label="stdout",
        value="(no log yet)",
        lines=30,
        max_lines=30,
        interactive=False,
        elem_id="log-box",
    )

    # ---- Event handlers ----

    def on_start():
        msg, _ = start_training()
        return msg

    def on_stop():
        msg, _ = stop_training()
        return msg

    def refresh():
        icon, elapsed, budget, loss, step, log = get_status()
        return icon, elapsed, budget, loss, step, log

    start_btn.click(fn=on_start, outputs=[msg_box])
    stop_btn.click(fn=on_stop, outputs=[msg_box])

    # Auto-refresh every 3 seconds
    timer = gr.Timer(value=3)
    timer.tick(
        fn=refresh,
        outputs=[status_icon, elapsed_box, budget_box, loss_box, step_box, log_box],
    )

    # Initial load
    demo.load(
        fn=refresh,
        outputs=[status_icon, elapsed_box, budget_box, loss_box, step_box, log_box],
    )


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    # Auto-start training in background when Space boots
    threading.Thread(target=_maybe_autostart, daemon=True).start()

    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        show_error=True,
    )