somatize.record
What happened, read back.
A Recorder writes one record per forward; this is how it is read:
from somatize import Recorder, Storefrom somatize.record import curve, forwards, nodes, runs
store = Store("/scratch/runs")Trainer(g, objective=..., optimizer=..., watching=Recorder(store, run="tuesday", summarising=["loss"]))
runs(store) # what is in here at allforwards(store, run="tuesday") # step by step, one scancurve(store, run="tuesday") # the losses, one scannodes(store, run="tuesday", last=50) # who spent the time, a fetch eachFunctions and not a type, like gather and take: what is being read is a
folder, and a class around a store would only be the store with a longer name.
There are two ways to see a run and they are not rivals. While it is going, what
you want arrives at watching= and costs nothing; when it is over — or when it
is another machine’s — there is no connection and a scan is all there is.
Both answer in the same shape, so whatever draws one draws the other.
Classes
Section titled “Classes”Live(*, title: str = 'live', smooth: int | None = None, every: int = 1)A run drawn while it happens. Hand it to watching=:
live = Live()live # the cell shows itg.forward(x, watching=live)With ipywidgets it redraws in place; without one it still tallies
everything and figure() gives the same picture. It holds one row of numbers
per forward, so watching for an afternoon does not grow with it, and keeps
nothing on disk — a Live beside a Recorder is the normal pairing.
Live.figure
Section titled “Live.figure”Live.figure() -> FigureThe same figure progress draws, from what has arrived so far.
Live.widget
Section titled “Live.widget”Live.widget() -> AnyA FigureWidget that redraws in place, or None without ipywidgets.
Asked for by hand rather than made on construction: building one imports
ipywidgets, and a Live used from a script has no business needing it.
Functions
Section titled “Functions”curve(store: Store, *, run: str, of: str = 'loss.value') -> list[tuple[int, float]]One number per forward, as (forward, value) pairs — a training curve.
of names the field: loss.value when the recorder summarised loss, and
otherwise anything a fact carries. Free when the field is in the record and
one fetch per forward when it is not, which curve_costs says out loud.
curve_costs
Section titled “curve_costs”curve_costs(store: Store, *, run: str, of: str = 'loss.value') -> strWhether curve would scan or fetch: "scan" or "fetch".
Asked before drawing something ten thousand steps long, and the answer is
"fetch" when the recorder was not told to summarise that kind.
facts(store: Store, *, run: str, forward: int) -> list[Fact] | NoneEverything that happened in one forward, in the order it arrived. The
detail, and the one call that costs a fetch. Each fact is the same dict a
watching= callable was handed, so what you looked at live is what you read
back. None for no such forward, which is not a failure.
fleet(store: Store, *, run: str, last: int | None = None) -> list[Row]What each machine did, as the inverse of nodes. The record is written
run → forward → node with where as an attribute, and this turns it the
other way up.
There is no registry and no heartbeat. A machine is here because it did something, and what it did is already written down.
busy, memory, cores, up_us and served are the half no record can
derive — the worker says them itself, down the connection that is already
open. The newest reading and not an average; None is a machine that did not
say.
waiting_us is the column that only exists up here: the round trip minus
what actually ran over there — the wire, the queue and the codec — and
neither half of that subtraction belongs to a node.
A scan and a fetch per forward, the same as nodes; last=N reads only
the last N.
gantt(store: Store, *, run: str, forward: int = 0) -> FigureOne forward on a timeline: what ran when, and what was waiting.
The picture spent cannot draw. A total says a node cost four hundred
milliseconds; this says whether those were beside the rest of the graph
or in front of it.
Every fact carries began_us, so a Wave shows as overlapping bars. A slice
that ran elsewhere counts from its own start and is shifted by the left
it arrived under — two wall clocks would not have composed. One forward and
not an average, because an average of timelines is not one.
machines
Section titled “machines”machines(store: Store, *, run: str, last: int | None = None) -> FigureThe run, per machine: what each one worked and what it was waited on.
The inverse of spent, and the split is the whole of it: a bar is the round
trip in two parts — time spent working, and time waited on, which is
the wire, the queue and the codec. Neither half belongs to a node, which is
why no per-node view can draw this. Costs what fleet costs.
forwards
Section titled “forwards”forwards(store: Store, *, run: str) -> list[Row]Every forward of that run, in order, as the record says it. One scan and
no fetches, which is what makes this what a progress view reads in a loop.
Anything the recorder was told to summarise comes back under its own
<kind>.<field> name, as text.
nodes(store: Store, *, run: str, last: int | None = None) -> list[Row]What each node did over the run, added up: how many times it ran, how long
in total and on average, how often it was read back instead, and where. It
costs a fetch per forward, because which node did what is in the blobs;
last=N reads only the last N.
progress
Section titled “progress”progress(store: Store, *, run: str, smooth: int | None = None) -> FigureHow a run went, step by step: the loss, the time, and what broke. One scan
when the recorder summarised the loss and a fetch per forward when it did
not — curve_costs says which. smooth is the window of the rolling mean,
in forwards; 0 draws only what was measured.
runs(store: Store) -> list[Row]Every run this store holds, newest last. One scan and no fetches: how many
forwards each has, how many broke, when the first and last were written,
and how long they took. The one to call first, because a store holds whatever
anybody put in it.
spent(store: Store, *, run: str, last: int | None = None) -> FigureWhere the time went, added up per node. Bars are coloured by where the
node ran, from the same table the graph is drawn with. A fetch per forward,
because which node did what is in the blobs; last=N reads only the last N.
standing
Section titled “standing”standing(store: Store) -> dict[str, Row]Every machine writing readings into this store, as {id: reading}.
The idle half: a worker says what it looks like on a clock whether or not anybody is asking it for anything, and it goes here rather than down a wire because a client only reads the socket while a job is in flight.
Keyed by what the machine calls itself, since on this path there is no
client and w1 is the client’s word; fleet joins the two. quiet_s is how
far behind the newest reading each one is, measured writer against writer
and never against this machine’s clock. One scan and no fetches.