trainmeter 0.0.2__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.
- trainmeter/__init__.py +17 -0
- trainmeter/__main__.py +5 -0
- trainmeter/agent/__init__.py +1 -0
- trainmeter/agent/bootstrap/sitecustomize.py +28 -0
- trainmeter/agent/core.py +578 -0
- trainmeter/agent/sender.py +68 -0
- trainmeter/agent/taps.py +144 -0
- trainmeter/agent/wrap.py +23 -0
- trainmeter/catalog.py +100 -0
- trainmeter/cli.py +382 -0
- trainmeter/commands.py +99 -0
- trainmeter/config.py +65 -0
- trainmeter/doctor.py +118 -0
- trainmeter/emit.py +64 -0
- trainmeter/engine.py +621 -0
- trainmeter/export/__init__.py +0 -0
- trainmeter/export/files.py +24 -0
- trainmeter/export/wandb.py +87 -0
- trainmeter/facts.py +50 -0
- trainmeter/flops.py +52 -0
- trainmeter/metrics.py +68 -0
- trainmeter/passport.py +327 -0
- trainmeter/peaks.py +92 -0
- trainmeter/records.py +97 -0
- trainmeter/replay.py +110 -0
- trainmeter/report.py +47 -0
- trainmeter/sources/__init__.py +1 -0
- trainmeter/sources/gpu.py +458 -0
- trainmeter/sources/host.py +135 -0
- trainmeter/supervisor/__init__.py +1 -0
- trainmeter/supervisor/ingest.py +86 -0
- trainmeter/supervisor/launcher.py +90 -0
- trainmeter/supervisor/live.py +133 -0
- trainmeter/supervisor/store.py +118 -0
- trainmeter/timeline.py +102 -0
- trainmeter/viewer.py +273 -0
- trainmeter/web/__init__.py +0 -0
- trainmeter/web/server.py +192 -0
- trainmeter/web/static/app.js +571 -0
- trainmeter/web/static/index.html +43 -0
- trainmeter/web/static/style.css +157 -0
- trainmeter-0.0.2.dist-info/METADATA +109 -0
- trainmeter-0.0.2.dist-info/RECORD +46 -0
- trainmeter-0.0.2.dist-info/WHEEL +4 -0
- trainmeter-0.0.2.dist-info/entry_points.txt +3 -0
- trainmeter-0.0.2.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Run a command as a child process and stay out of its way.
|
|
2
|
+
|
|
3
|
+
Contract: the child inherits stdio and is not touched; its exit status is returned as a shell
|
|
4
|
+
would report it (128+N when killed by signal N); SIGTERM, SIGHUP and SIGQUIT sent to `tm` alone
|
|
5
|
+
are forwarded to the child; Ctrl-C reaches both processes through the terminal, so SIGINT is not
|
|
6
|
+
forwarded and `tm` never exits before the child.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import signal
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Callable, Mapping
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
FORWARDED = ("SIGTERM", "SIGHUP", "SIGQUIT")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Outcome:
|
|
24
|
+
status: int
|
|
25
|
+
signal: int | None
|
|
26
|
+
elapsed_s: float
|
|
27
|
+
pid: int | None = None
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def exit_status(returncode: int) -> tuple[int, int | None]:
|
|
32
|
+
"""(shell-style status, killing signal or None) for a `Popen.returncode`."""
|
|
33
|
+
if returncode < 0:
|
|
34
|
+
return 128 - returncode, -returncode
|
|
35
|
+
return returncode, None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def launch(
|
|
39
|
+
argv: list[str],
|
|
40
|
+
*,
|
|
41
|
+
env: Mapping[str, str] | None = None,
|
|
42
|
+
on_spawn: Callable[[int], None] | None = None,
|
|
43
|
+
clock: Callable[[], float] = time.monotonic,
|
|
44
|
+
) -> Outcome:
|
|
45
|
+
state: dict[str, Any] = {"proc": None, "pending": []}
|
|
46
|
+
|
|
47
|
+
def forward(signum: int, _frame: Any) -> None:
|
|
48
|
+
proc = state["proc"]
|
|
49
|
+
if proc is None:
|
|
50
|
+
state["pending"].append(signum)
|
|
51
|
+
return
|
|
52
|
+
try:
|
|
53
|
+
proc.send_signal(signum)
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def ignore(_signum: int, _frame: Any) -> None:
|
|
58
|
+
# A Python handler, not SIG_IGN: an ignored signal would be inherited by the child.
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
previous: dict[int, Any] = {}
|
|
62
|
+
handlers = {getattr(signal, n): forward for n in FORWARDED if hasattr(signal, n)}
|
|
63
|
+
handlers[signal.SIGINT] = ignore
|
|
64
|
+
try:
|
|
65
|
+
for signum, handler in handlers.items():
|
|
66
|
+
previous[signum] = signal.signal(signum, handler)
|
|
67
|
+
except ValueError: # not the main thread: no handlers, plain child
|
|
68
|
+
previous.clear()
|
|
69
|
+
|
|
70
|
+
start = clock()
|
|
71
|
+
try:
|
|
72
|
+
try:
|
|
73
|
+
proc = subprocess.Popen(argv, env=None if env is None else dict(env))
|
|
74
|
+
except FileNotFoundError as exc:
|
|
75
|
+
return Outcome(127, None, clock() - start, error=str(exc))
|
|
76
|
+
except OSError as exc:
|
|
77
|
+
return Outcome(126, None, clock() - start, error=str(exc))
|
|
78
|
+
state["proc"] = proc
|
|
79
|
+
for signum in state["pending"]:
|
|
80
|
+
forward(signum, None)
|
|
81
|
+
if on_spawn is not None:
|
|
82
|
+
try:
|
|
83
|
+
on_spawn(proc.pid)
|
|
84
|
+
except Exception as exc: # noqa: BLE001 - monitoring must not touch the child
|
|
85
|
+
warnings.warn(f"trainmeter monitoring failed to start: {exc!r}", stacklevel=2)
|
|
86
|
+
status, killed_by = exit_status(proc.wait())
|
|
87
|
+
return Outcome(status, killed_by, clock() - start, pid=proc.pid)
|
|
88
|
+
finally:
|
|
89
|
+
for signum, handler in previous.items():
|
|
90
|
+
signal.signal(signum, handler)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Live state behind the dashboard: the engine and the timeline, fed under one lock.
|
|
2
|
+
|
|
3
|
+
Records come from the ingest thread and the samplers; the web threads only read. A ticker takes
|
|
4
|
+
one snapshot per second, samples the derived metrics into the timeline and keeps the snapshot
|
|
5
|
+
for readers, so a request never waits on the engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import warnings
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from ..engine import Engine
|
|
17
|
+
from ..timeline import Timeline
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LiveState:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
engine: Engine,
|
|
24
|
+
header: dict[str, Any] | None = None,
|
|
25
|
+
mono: Callable[[], float] = time.monotonic,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.engine = engine
|
|
28
|
+
self.timeline = Timeline()
|
|
29
|
+
self.header = header or {}
|
|
30
|
+
self._mono = mono
|
|
31
|
+
self.t0 = mono()
|
|
32
|
+
self.lock = threading.Lock()
|
|
33
|
+
self.running = True
|
|
34
|
+
self.exit: dict[str, Any] | None = None
|
|
35
|
+
self.replay: str | None = None # the run directory name when this is a replay
|
|
36
|
+
self._latest: dict[str, Any] = self._envelope({})
|
|
37
|
+
self._stop = threading.Event()
|
|
38
|
+
self._thread = threading.Thread(target=self._run, name="trainmeter-live", daemon=True)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_replay(cls, replayed: Any) -> LiveState:
|
|
42
|
+
"""A finished run, served as it ended: no ticker, no clock, the log's own numbers."""
|
|
43
|
+
live = cls(replayed.engine, replayed.header, mono=lambda: replayed.t_end)
|
|
44
|
+
live.t0 = replayed.t_start
|
|
45
|
+
live.timeline = replayed.timeline
|
|
46
|
+
live.replay = replayed.path.name
|
|
47
|
+
live.running = False
|
|
48
|
+
live.exit = replayed.exit
|
|
49
|
+
live._latest = live._envelope(replayed.snapshot)
|
|
50
|
+
return live
|
|
51
|
+
|
|
52
|
+
def feed(self, record: dict[str, Any]) -> None:
|
|
53
|
+
with self.lock:
|
|
54
|
+
self.engine.feed(record)
|
|
55
|
+
self.timeline.feed(record, record["t"] - self.t0, self.engine.position())
|
|
56
|
+
|
|
57
|
+
def _envelope(self, snap: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
elapsed = self._mono() - self.t0
|
|
59
|
+
out: dict[str, Any] = {
|
|
60
|
+
"snapshot": snap,
|
|
61
|
+
"elapsed_s": elapsed,
|
|
62
|
+
"running": self.running,
|
|
63
|
+
"exit": self.exit,
|
|
64
|
+
"replay": self.replay,
|
|
65
|
+
}
|
|
66
|
+
n_gpus = (snap.get("n_gpus") or {}).get("value")
|
|
67
|
+
if n_gpus:
|
|
68
|
+
out["gpu_hours"] = elapsed * n_gpus / 3600 # view-time; the price stays in the browser
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
def tick(self) -> None:
|
|
72
|
+
"""One snapshot: kept for readers and sampled into the timeline. Never raises."""
|
|
73
|
+
try:
|
|
74
|
+
with self.lock:
|
|
75
|
+
snap = self.engine.snapshot(self._mono())
|
|
76
|
+
t = self._mono() - self.t0
|
|
77
|
+
self.timeline.add_snapshot(snap, t, self.engine.position())
|
|
78
|
+
self._latest = self._envelope(snap)
|
|
79
|
+
except Exception as exc: # noqa: BLE001 - the dashboard must not cost a run
|
|
80
|
+
warnings.warn(f"trainmeter: snapshot failed: {exc!r}", stacklevel=2)
|
|
81
|
+
|
|
82
|
+
def latest(self) -> dict[str, Any]:
|
|
83
|
+
with self.lock:
|
|
84
|
+
return self._latest
|
|
85
|
+
|
|
86
|
+
def facts(self) -> dict[str, Any]:
|
|
87
|
+
with self.lock:
|
|
88
|
+
return {
|
|
89
|
+
k: {"value": f.value, "source": f.source, "note": f.note}
|
|
90
|
+
for k, f in self.engine.facts.items().items()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def passport(self) -> dict[str, Any]:
|
|
94
|
+
"""The run passport of the latest snapshot, plus its Markdown."""
|
|
95
|
+
from .. import passport
|
|
96
|
+
|
|
97
|
+
with self.lock:
|
|
98
|
+
snap = self._latest["snapshot"]
|
|
99
|
+
data = passport.build(
|
|
100
|
+
snap, self.header, self.facts_unlocked(), self.engine.hello, self.exit
|
|
101
|
+
)
|
|
102
|
+
return {"passport": data, "markdown": passport.to_markdown(data)}
|
|
103
|
+
|
|
104
|
+
def facts_unlocked(self) -> dict[str, Any]:
|
|
105
|
+
return {
|
|
106
|
+
k: {"value": f.value, "source": f.source, "note": f.note}
|
|
107
|
+
for k, f in self.engine.facts.items().items()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def series(self, names: list[str], x: str, since: float | None) -> dict[str, Any]:
|
|
111
|
+
with self.lock:
|
|
112
|
+
return self.timeline.query(names, x, since)
|
|
113
|
+
|
|
114
|
+
def series_names(self) -> list[str]:
|
|
115
|
+
with self.lock:
|
|
116
|
+
return self.timeline.names()
|
|
117
|
+
|
|
118
|
+
def finish(self, exit_info: dict[str, Any]) -> None:
|
|
119
|
+
self.running = False
|
|
120
|
+
self.exit = exit_info
|
|
121
|
+
self.tick()
|
|
122
|
+
|
|
123
|
+
def start(self) -> None:
|
|
124
|
+
self._thread.start()
|
|
125
|
+
|
|
126
|
+
def stop(self) -> None:
|
|
127
|
+
self._stop.set()
|
|
128
|
+
if self._thread.is_alive():
|
|
129
|
+
self._thread.join(timeout=2.0)
|
|
130
|
+
|
|
131
|
+
def _run(self) -> None:
|
|
132
|
+
while not self._stop.wait(1.0):
|
|
133
|
+
self.tick()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""The run directory and its single writer.
|
|
2
|
+
|
|
3
|
+
`events.jsonl` is appended by the supervisor only. Every failure here is reported once and
|
|
4
|
+
then swallowed: a full disk must never cost a training run.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, TextIO
|
|
18
|
+
|
|
19
|
+
from ..records import encode, make_record
|
|
20
|
+
|
|
21
|
+
RUNS_DIR = Path(".trainmeter") / "runs"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_name(name: str) -> str:
|
|
25
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "-", name).strip("-.") or "run"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunStore:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
path: Path,
|
|
32
|
+
mono: Callable[[], float] = time.monotonic,
|
|
33
|
+
wall: Callable[[], float] = time.time,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.path = path
|
|
36
|
+
self.run_id = path.name
|
|
37
|
+
self._mono = mono
|
|
38
|
+
self.mono0 = mono()
|
|
39
|
+
self.wall0 = wall()
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
self._warned = False
|
|
42
|
+
self._file: TextIO | None = open(path / "events.jsonl", "a", encoding="utf-8") # noqa: SIM115
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def create(
|
|
46
|
+
cls,
|
|
47
|
+
base: Path,
|
|
48
|
+
name: str,
|
|
49
|
+
mono: Callable[[], float] = time.monotonic,
|
|
50
|
+
wall: Callable[[], float] = time.time,
|
|
51
|
+
) -> RunStore:
|
|
52
|
+
"""Create `<base>/.trainmeter/runs/<UTC timestamp>-<name>/`, adding a suffix on a clash."""
|
|
53
|
+
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(wall()))
|
|
54
|
+
runs = Path(base) / RUNS_DIR
|
|
55
|
+
runs.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
stem = f"{stamp}-{_safe_name(name)}"
|
|
57
|
+
for n in range(1, 1000):
|
|
58
|
+
path = runs / (stem if n == 1 else f"{stem}-{n}")
|
|
59
|
+
try:
|
|
60
|
+
path.mkdir()
|
|
61
|
+
except FileExistsError:
|
|
62
|
+
continue
|
|
63
|
+
return cls(path, mono, wall)
|
|
64
|
+
raise FileExistsError(f"no free run directory for {stem}")
|
|
65
|
+
|
|
66
|
+
def _warn_once(self, message: str) -> None:
|
|
67
|
+
if not self._warned:
|
|
68
|
+
self._warned = True
|
|
69
|
+
warnings.warn(f"trainmeter run log: {message}", stacklevel=3)
|
|
70
|
+
|
|
71
|
+
def emit(
|
|
72
|
+
self, kind: str, src: str, d: dict[str, Any] | None = None, *, pid: int | None = None
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Append one record. Never raises."""
|
|
75
|
+
try:
|
|
76
|
+
line = encode(make_record(kind, src, self._mono(), d, pid=pid))
|
|
77
|
+
except (TypeError, ValueError) as exc:
|
|
78
|
+
self._warn_once(f"dropped a record that is not valid JSON: {exc}")
|
|
79
|
+
return
|
|
80
|
+
self._write_line(line)
|
|
81
|
+
|
|
82
|
+
def _write_line(self, line: str) -> None:
|
|
83
|
+
with self._lock:
|
|
84
|
+
if self._file is None:
|
|
85
|
+
return
|
|
86
|
+
try:
|
|
87
|
+
self._file.write(line + "\n")
|
|
88
|
+
self._file.flush()
|
|
89
|
+
except OSError as exc:
|
|
90
|
+
self._warn_once(f"stopped writing: {exc}")
|
|
91
|
+
self._file = None
|
|
92
|
+
|
|
93
|
+
def append_record(self, record: dict[str, Any]) -> None:
|
|
94
|
+
"""Append a record that already has its envelope, such as one received from an agent."""
|
|
95
|
+
try:
|
|
96
|
+
line = encode(record)
|
|
97
|
+
except (TypeError, ValueError) as exc:
|
|
98
|
+
self._warn_once(f"dropped a record that is not valid JSON: {exc}")
|
|
99
|
+
return
|
|
100
|
+
self._write_line(line)
|
|
101
|
+
|
|
102
|
+
def write_json(self, name: str, obj: dict[str, Any]) -> None:
|
|
103
|
+
"""Write `name` atomically. Never raises."""
|
|
104
|
+
try:
|
|
105
|
+
tmp = self.path / f".{name}.tmp"
|
|
106
|
+
tmp.write_text(json.dumps(obj, indent=2) + "\n", encoding="utf-8")
|
|
107
|
+
os.replace(tmp, self.path / name)
|
|
108
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
109
|
+
self._warn_once(f"could not write {name}: {exc}")
|
|
110
|
+
|
|
111
|
+
def close(self) -> None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
if self._file is not None:
|
|
114
|
+
try:
|
|
115
|
+
self._file.close()
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
self._file = None
|
trainmeter/timeline.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Chart history in memory: named series of points that can be read against any x axis.
|
|
2
|
+
|
|
3
|
+
Pure. It is fed from the same records as the engine, so a replay of a run log rebuilds it.
|
|
4
|
+
The x axes are `step`, `tokens`, `flops` (modeled FLOPs spent) and `time` (seconds since the
|
|
5
|
+
run started). A point whose x is unknown, such as `flops` before a FLOPs per token exists, is
|
|
6
|
+
left out of that axis instead of being placed at zero.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import catalog
|
|
14
|
+
|
|
15
|
+
AXES = ("step", "tokens", "flops", "time")
|
|
16
|
+
_LADDER = ("gpu_util", "sm_active", "tensor_active")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Timeline:
|
|
20
|
+
def __init__(self, max_points: int = 4000) -> None:
|
|
21
|
+
self._max = max_points
|
|
22
|
+
# name -> list of (time, step, tokens, flops, value)
|
|
23
|
+
self._series: dict[str, list[tuple[float, Any, Any, Any, float]]] = {}
|
|
24
|
+
self._explicit_train = False
|
|
25
|
+
|
|
26
|
+
def add(self, name: str, value: Any, t: float, pos: dict[str, Any]) -> None:
|
|
27
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or value != value:
|
|
28
|
+
return
|
|
29
|
+
if value in (float("inf"), float("-inf")):
|
|
30
|
+
return
|
|
31
|
+
points = self._series.setdefault(name, [])
|
|
32
|
+
points.append((t, pos.get("step"), pos.get("tokens"), pos.get("flops"), float(value)))
|
|
33
|
+
if len(points) > self._max:
|
|
34
|
+
del points[1:-1:2] # halve the resolution, keep the first and the last point
|
|
35
|
+
|
|
36
|
+
# ---- feeding -----------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def feed(self, record: dict[str, Any], t: float, pos: dict[str, Any]) -> None:
|
|
39
|
+
"""`t` is seconds since the run started, `pos` the engine's position after the record."""
|
|
40
|
+
d = record.get("d") or {}
|
|
41
|
+
kind = record.get("kind")
|
|
42
|
+
if kind == "metric" and isinstance(d.get("key"), str):
|
|
43
|
+
here = dict(pos)
|
|
44
|
+
if isinstance(d.get("step"), int):
|
|
45
|
+
here["step"] = d["step"]
|
|
46
|
+
key = d["key"]
|
|
47
|
+
if catalog.canonical(key) == "train_loss":
|
|
48
|
+
self._explicit_train = True
|
|
49
|
+
self.add(key, d.get("value"), t, here)
|
|
50
|
+
elif kind == "sample.gpu":
|
|
51
|
+
for row in d.get("gpus") or []:
|
|
52
|
+
for field, *_ in catalog.GPU_FIELDS:
|
|
53
|
+
if field in row:
|
|
54
|
+
self.add(f"gpu.{row.get('index')}.{field}", row[field], t, pos)
|
|
55
|
+
|
|
56
|
+
def add_snapshot(self, snap: dict[str, Any], t: float, pos: dict[str, Any]) -> None:
|
|
57
|
+
"""The derived metrics, sampled once per tick."""
|
|
58
|
+
for name in ("tokens_per_s", "step_time_s", "progress", "flops_spent"):
|
|
59
|
+
self.add(name, snap.get(name), t, pos)
|
|
60
|
+
for name in ("mfu", "ofu", "mfu_ofu_gap", "arithmetic_intensity", "flops_observed"):
|
|
61
|
+
self.add(name, (snap.get(name) or {}).get("value"), t, pos)
|
|
62
|
+
self.add("goodput", (snap.get("time") or {}).get("goodput"), t, pos)
|
|
63
|
+
host = snap.get("host") or {}
|
|
64
|
+
self.add("host_cpu_cores", host.get("cpu_cores"), t, pos)
|
|
65
|
+
self.add("host_rss_b", host.get("rss_b"), t, pos)
|
|
66
|
+
mean = (snap.get("ladder") or {}).get("mean") or {}
|
|
67
|
+
for name in _LADDER:
|
|
68
|
+
self.add(name, mean.get(name), t, pos)
|
|
69
|
+
|
|
70
|
+
# ---- reading -----------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _keys_for(self, name: str) -> str | None:
|
|
73
|
+
"""The stored key behind a canonical name: the name itself, or the first alias seen."""
|
|
74
|
+
if name in self._series:
|
|
75
|
+
return name
|
|
76
|
+
for key in self._series:
|
|
77
|
+
if catalog.canonical(key) == name:
|
|
78
|
+
return key
|
|
79
|
+
if name == "train_loss" and not self._explicit_train and catalog.BARE_LOSS in self._series:
|
|
80
|
+
return catalog.BARE_LOSS
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def names(self) -> list[str]:
|
|
84
|
+
return sorted(self._series)
|
|
85
|
+
|
|
86
|
+
def query(
|
|
87
|
+
self, names: list[str], x: str = "step", since: float | None = None
|
|
88
|
+
) -> dict[str, list[list[float]]]:
|
|
89
|
+
if x not in AXES:
|
|
90
|
+
raise ValueError(f"x must be one of {', '.join(AXES)}")
|
|
91
|
+
column = {"time": 0, "step": 1, "tokens": 2, "flops": 3}[x]
|
|
92
|
+
out: dict[str, list[list[float]]] = {}
|
|
93
|
+
for name in names:
|
|
94
|
+
key = self._keys_for(name)
|
|
95
|
+
points = self._series.get(key, []) if key else []
|
|
96
|
+
rows = [
|
|
97
|
+
[p[column], p[4]]
|
|
98
|
+
for p in points
|
|
99
|
+
if p[column] is not None and (since is None or p[column] > since)
|
|
100
|
+
]
|
|
101
|
+
out[name] = rows
|
|
102
|
+
return out
|