File size: 4,898 Bytes
00c55c8 | 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 | """Parse training log file and write metrics to TensorBoard event files.
Usage:
# One-shot conversion
python scripts/log_to_tensorboard.py --log /tmp/streampi_t3_robodojo_30k_v1_20260909.log
# Watch mode (continuously monitor log for new entries)
python scripts/log_to_tensorboard.py --log /tmp/streampi_t3_robodojo_30k_v1_20260909.log --watch
# Custom output directory
python scripts/log_to_tensorboard.py --log /tmp/run.log --output-dir runs/my_run
"""
import argparse
import os
import re
import time
from pathlib import Path
try:
from torch.utils.tensorboard import SummaryWriter
except ImportError:
from tensorboardX import SummaryWriter
def parse_step_line(line: str) -> dict | None:
"""Parse a log line like 'Step 0: grad_norm=4.0662, loss=0.4126, param_norm=1802.3864'.
Returns a dict with 'step' (int) and metric key-value pairs (float), or None.
"""
match = re.match(r"Step\s+(\d+):\s*(.+)", line.strip())
if not match:
return None
step = int(match.group(1))
metrics_str = match.group(2)
result: dict = {"step": step}
for pair in metrics_str.split(","):
pair = pair.strip()
if "=" not in pair:
continue
key, value = pair.split("=", 1)
key = key.strip()
value = value.strip()
try:
result[key] = float(value)
except ValueError:
continue
return result
def parse_log_file(log_path: str, last_step: int = -1) -> list[dict]:
"""Return deduplicated entries with step > last_step.
If a resumed run repeats a step, the last occurrence in the log is kept,
because it belongs to the trajectory resumed from the latest checkpoint.
"""
entries_by_step: dict[int, dict] = {}
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
parsed = parse_step_line(line)
if parsed is None:
continue
step = parsed["step"]
if step <= last_step:
continue
# Later occurrences replace pre-resume entries at the same step.
entries_by_step[step] = parsed
return [entries_by_step[step] for step in sorted(entries_by_step)]
def write_entries(writer: SummaryWriter, entries: list[dict]) -> int:
"""Write parsed entries to TensorBoard. Returns the max step written."""
max_step = -1
for entry in entries:
step = entry["step"]
for key, value in entry.items():
if key == "step":
continue
writer.add_scalar(key, value, step)
max_step = max(max_step, step)
writer.flush()
return max_step
def main():
parser = argparse.ArgumentParser(description="Convert training log to TensorBoard events.")
parser.add_argument("--log", type=str, required=True, help="Path to the training log file.")
parser.add_argument(
"--output-dir",
type=str,
default=None,
help="TensorBoard output directory. Defaults to runs/<log_filename_stem>.",
)
parser.add_argument(
"--watch",
action="store_true",
help="Continuously monitor the log file for new entries.",
)
parser.add_argument(
"--interval",
type=float,
default=10.0,
help="Polling interval in seconds for --watch mode (default: 10).",
)
args = parser.parse_args()
log_path = args.log
if not os.path.exists(log_path):
raise FileNotFoundError(f"Log file not found: {log_path}")
log_stem = Path(log_path).stem
output_dir = args.output_dir or os.path.join("runs", log_stem)
os.makedirs(output_dir, exist_ok=True)
writer = SummaryWriter(log_dir=output_dir)
print(f"TensorBoard log dir: {os.path.abspath(output_dir)}")
print(f"Monitoring log: {log_path}")
# One-shot parse
entries = parse_log_file(log_path)
max_step = write_entries(writer, entries)
print(f"Wrote {len(entries)} entries (max step: {max_step})")
if not args.watch:
writer.close()
print(f"\nDone. Run: tensorboard --logdir {os.path.abspath(output_dir)}")
return
# Watch mode
print(f"Watching for new entries (interval={args.interval}s). Press Ctrl+C to stop.")
try:
while True:
time.sleep(args.interval)
new_entries = parse_log_file(log_path, last_step=max_step)
if new_entries:
max_step = write_entries(writer, new_entries)
print(f" Updated: +{len(new_entries)} entries (max step: {max_step})")
except KeyboardInterrupt:
print("\nStopping watch mode.")
finally:
writer.close()
print(f"TensorBoard events written to: {os.path.abspath(output_dir)}")
print(f"Run: tensorboard --logdir {os.path.abspath(output_dir)}")
if __name__ == "__main__":
main() |