moazzamak/dsh-train-dashboard

dsh-train-dashboard

A right-pane training dashboard for the DeepSeek Harness: runs a command of yours that writes a JSON snapshot of a training run, and charts it, one series at a time, with the age of the numbers always on screen.

未分類chartsdashboarddeepseek-harness

dsh-train-dashboard

A training dashboard for the DeepSeek Harness (DSH). It adds a Training dashboard tab to the right pane, beside Files, Terminal and Document, and charts a JSON snapshot of a training run: a KPI header, one series at a time on its own axis, the joints that make the series incomparable marked on the chart with the reason for each, and the model's own growth drawn against the reading it buys.

The plugin does not parse your logs. It runs a command of your own, that command writes the snapshot JSON described below, and the tab reads it. So the plugin works with any training setup whose numbers you can get into that shape, and it never competes with your own parser for the truth about a run.

  • no build step, no runtime dependencies, plain JavaScript in two files
  • no process at all unless a tab is open and the snapshot is older than refreshSeconds
  • never writes anything itself: the snapshot is written by your command, and it is the only file the plugin causes to be written
  • the age of the numbers is always on screen, so a stale chart can never be mistaken for a live one
  • every word a break marker shows is your snapshot's own: the tab chooses where a marker sits and how it reads, never what it says

Package version 0.4.0 — snapshot contract version 1.

What changed in 0.4.0

Two features were removed and one defect that had been hiding behind them was fixed. The snapshot contract number stays at 1: every change is additive or ignored — a snapshot that carries a key this version no longer reads is still served, and still drawn as far as it can be.

  • No more predictions. The dashed frontier line, its register block, its switch and the frontier key are gone. A producer's phase length is not knowable in advance — a phase ends when its read goes flat, not at a fixed leg count — so the projection was wrong in its premise, not merely imprecise. A snapshot that still serves frontier is served and the key is ignored.
  • No more NO KIND marker. The legacy single-break key break_leg — one constant naming a joint of an arc the snapshot no longer charts, carrying no kind — is no longer folded in, and /state no longer republishes it as breakLeg. A marker whose whole content is that it claims nothing, drawn on a run that never trained that leg, is not a joint a reader can use.
  • A growth now has a colour and a shape in the key. The growth kind was added to the chart's shape and ink tables and to no rule in the stylesheet, so its register swatch fell back to the sheet's grey and its key drew no shape at all — a mark on the chart the register could not decode.
  • The KPI header reads the current bpb tags. The producer renamed train/bpb_sealed and train/bpb_legval to train/bpb and train/heldout_bpb on 11 October 2026, and this tab's headline list was not moved with them: the headline resolved to nothing and the first two KPI tiles read — beside four that filled normally. Both spellings are recognised now, current first, so an old snapshot on disk still charts.

What you get

regioncontent
headerthe run's name, the snapshot's age with a fresh/stale badge, a Refresh button
KPI rowlatest step, the held-out reading, step wall time, throughput, memory peak, evaluation accuracy, the seed model's size
chipsthe four metrics this tab leads with, then a N more series disclosure holding everything else the snapshot carries
chartONE series at a time on its own axis, with every break marker drawn on it, over a timeline you can select — plus the seed model's size as a second dashed line against the axis on the right, the chain a rollback threw away ghosted beside the live one on a shared origin, and a floating current value for each line
registerunder the chart: a switch per break category, then one status line per marker — its kind's shape and colour, its word, its leg and what changed — with the reason, the meaning and the evidence behind a per-row why disclosure
footerthe snapshot path being read

One series at a time is deliberate. Series carry different units — bits per byte, seconds, GiB, accuracy, counts — and drawing them on one axis would invite exactly the comparison the axis cannot support. There is exactly one exception, and it is the next section.

The seed model's size is that one exception, drawn on the same frame against an axis of its own on the right — the growth ladder is the point of a run that adds capacity as it trains, so how big the model has become belongs on screen at the same time as the reading it buys. See The seed model's size for what the number is and — more importantly — what it deliberately is not.

The chips are a short list on purpose. A snapshot of this run carries sixty series, and a chip for every one of them is a wall of controls over a chart that draws one curve. The tab leads with the four it is steered by — the leg's own end-of-leg reading, throughput, VRAM peak and the exam — and the other thirty-six are one click away behind the disclosure. Nothing is dropped: a producer's own tag is still reachable, labelled by its own name.

One curve, not two. train/bpb_sealed and train/bpb_legval are the same chart. Measured on this run's snapshot: the curve carries 688 readings over legs 1–689 and the leg-end reading 730 over 1–731, and on all 688 legs they share the values are identical. The curve stops at the phase boundary, which is where the corpus transfer happens, and the leg-end reading keeps going for another 42 legs. Both are written by the producer, so the tab resolves the pair: where the leg-end reading exists the truncated copy is not offered at all, and where a producer writes only the curve the curve is the held-out reading. The KPI header names which of the two it read.

One series at a time is deliberate. Series carry different units — bits per byte, seconds, GiB, accuracy, counts — and drawing them on one axis would invite exactly the comparison the axis cannot support. There is exactly one exception, and it is the next section.

The seed model's size

The growth ladder is the point of a run that adds capacity as it trains, so how big the model has become belongs on screen at the same time as the reading it buys. A chip would have made you choose between the two, which is the one comparison you are trying to make. So it is drawn as a second, dashed line on the same frame, against an axis on the RIGHT:

  • the frame WIDENS by a gutter only when there is a second line, and the gutter is where the second axis' tick labels live — never on top of the curve;
  • the two axes share one set of horizontal gridlines, and their spacing is the scale's: exponential when the second line is logarithmic, regular when it is not. Every line carries a value from both scales, so the grid is a set of reference lines a reader can read twice rather than two grids interleaved — see below;
  • the second axis is logarithmic, and is scaled to the whole size series rather than to the window — see below;
  • the legend under the chart names both series, both units and the side of the frame each one's scale sits on;
  • each line's current value floats in the left margin, written at the height that line was drawn at and in that line's own colour — so a number and the curve it belongs to are read together, with no legend in between. It sits left of the plot, in a column of its own separated from the axis numbers by position, colour and a short rail down its inner edge, because a reading that shares the numbers' column is mistaken for a tick. It is the newest reading of the whole run, not of the window: a reader zoomed into leg 300 is still steering by the latest number, and a reading that vanished when they panned is one they cannot rely on. A reading is only written where its line is actually on screen, and the two are held at least 13 units apart and inside the frame — the size axis barely moves while a bpb curve swings, so the two newest readings often land at nearly one height;
  • the line is resampled onto the selected series' x values, so both lines are drawn in one coordinate system. Where the size series has no reading, the line stops rather than carrying the last value forward: a line that continues past its last measurement claims the model was that size at legs nobody measured.

One grid, two scales, and the spacing follows the scale kind. The frame previously drew a grid per axis, at two different spacings over one another, and half of each line belonged to no label — a texture rather than a grid. There is now one set of lines:

second linelines atexample from this run
logarithmicthe log axis' marks — equal ratios apart, so larger steps in value the further up the frame1.9897 bpb / 17.4M · 2.3737 / 19.1M · 2.7576 bpb / 20.9M params
linearthe primary axis' own ticks — equal stepsevenly spaced

So the horizontal lines are at regular intervals on a linear scale and at exponential ones on a log scale, and both axes quote every line. A line's value on the primary axis is solved from that axis' own projection (by bisection, not by a second formula that could disagree with the drawing), so a gridline's label can never name a value the line is not on. Two labels that would round alike take one more decimal until they do not.

Each axis names its unit exactly once, in the title rotated up its outer edge (bpb, params). The tick labels beside it are bare numbers — 17.4M, not 17.4M params — because a unit written on a label as well says the same word twice within a few pixels of itself. The scale mark the value formatter writes (22.2M) stays with the numbers, where a reader comparing two ticks needs it; the rotated title carries the word those letters stand for; and the legend spells both units out in full for anyone who wants them named.

The size axis does not follow the window, and that is deliberate. This line exists to show growth, and the size only ever rises — so a floor taken from the samples in the window rises as the reader pans right and lifts the beginning of the line off the bottom of the frame. Zoomed into the late run, the early readings fall below the axis and stop being drawn, while every count on screen still says they were drawn: a series that appears to begin later than it does, with nothing saying a reading was dropped. Anchored to the whole series instead, the floor stays put under every window and the line's shape is comparable from one window to the next. The x window still does all the framing work it did before. The main axis keeps its window rescaling, because its readings are noisy and uncorrelated — there the rescale is the point.

Why log. The size is the product of the growth ladder, and the ladder multiplies: this run steps 17.05M → 22.22M, which is 1.30×. A linear axis anchored at zero draws that as 23% of the frame at most, and the ladder itself — every growth this pass has bought — as under 2% of it, which is a flat line near the top. On a log axis the same readings are 0.115 decades and the run uses 92% of the height, and each growth step is the same fraction of the height however big the model already is.

A log axis also has no zero: log(0) is negative infinity, so a floor of 0 is not a setting the axis can honour. Its floor instead sits 8% of the height below the run's smallest reading, which is where a linear axis' zero would be.

  • the floor is solved from the data's own span, so it works on a narrow window exactly as on the whole run. Placing it at a fixed fraction of the maximum instead — the obvious first attempt — leaves a 1.30× run drawn in the top 19% of the frame, the same flattening the log axis was chosen to fix.
  • a value outside the axis is dropped, never clamped: train/seed_read_rows really does start at 0 rows, and folding that to the bottom of the frame would draw a zero on a scale that cannot contain one. It is one switch away if you want to watch the read grow from nothing.
  • the marks are evenly spaced powers of ten — 17M 19M 21M on this run — and never two of them closer than 8% of the height, because two marks that close are one blur rather than a scale.
  • the axis carries no caption: an earlier version printed a sentence explaining what a log scale is, and it was removed as clutter. The marks are what say it, and the legend still names both lines, both units and both sides.

The series it looks for, best source first:

tagdrawn asunit
train/seed_model_paramsthe headline: the whole modelM params
train/seed_read_rowsthe part a growth deposit movesrows
train/seed_trainable_rowsthe fixed table beside itrows

A producer that serves none of them gets no second line and no second axis — never an empty axis, which would be furniture. The same three tags are kept out of the chip list: a chip that selected a series already drawn underneath the curve would be a second way to ask for one picture.

What this number is, and what it is not. It is a count of PARAMETERS of the whole model — the tick table plus the read's live rows — and two cheaper measurements are wrong on a run of this kind:

  • the checkpoint's byte count, because the read is a fixed allocation rectangle: every leg writes the same file size (measured: 375,398,237 bytes on legs 800 to 893) while the model inside it grows. A byte-count line would be perfectly flat over a model that is getting bigger.
  • the read's allocation, because an allocated-but-not-live row is held at exactly zero and given no update, so it "contributes nothing to the read". On leg 66 of the live pass the read was allocated 4,096 rows and 385 of them were live: pricing the allocation put 3.15M where the truth was 0.30M, an overstatement of ten times, and mixing the two made the size series dip from 20.1M to 17.1M and rise again — a shape no growing model makes.

So the size is counted from the two tables that do move, each at its own price:

  1. the tick table: born-rows gate ON: N trainable rows, where one row is an embedding row of d numbers and an unembedding row of 2d + 1, so one row is 3d + 1 = 577 parameters and not one. Pricing a row at a single parameter is what made this chart say 4.0M about a 22.2M model.
  2. the read's live rows, at the trainer's own READ_PARAMS_PER_ROW — a key row and a value row of 2d + 1 numbers each.

Both prices come from the trainer's registered constants rather than from arithmetic restated here. A second derivation would be free to disagree with the trainer about how wide the model is, and then the chart and the log would tell different stories about the size of one model. Cross-checked against the live checkpoint at leg 893: E (29035, 192) = 5,574,720, U (29035, 385) = 11,178,475, read 7,096 × 770 = 5,463,920, total 22,217,115 — which is what the chart says.

The live count is only ever read from a line that states it. Two lines do: the gate's read live-row gate ON: N of M allocated rows are live, and the packed read's groups G x N LIVE rows of M allocated. A leg whose log carries neither — legs 1 to 65, 67 to 124 and 129 to 278 of the live pass, which were trained before the trainer printed a live count every leg — gets no reading at all. That becomes a gap, and it is deliberate: the live set only ever grows (rows are appended, never removed), so a dip in the series is always a misread, and the honest drawing of "this leg did not say" is nothing rather than an interpolation between its neighbours.

Sizing a series the snapshot never reported. A leg whose log says nothing about the size is not charted at all: the series has no point there, and if NO leg reports a size there is no size series. Where the first readable leg is not the run's first, one explicit point is emitted for the leg before it at the same size, labelled as back-projected in the series' source note, so a growth reads as a step at the leg that took it rather than as the line's own start.

The size series is not split at the break — unlike every metric series — and that is deliberate. The break is a change in the arithmetic a leg computes, which is why bits per byte is not comparable across it. A parameter count is a statement about the model's shape: it means the same thing on both sides, and splitting it would draw two half-lines for one continuous quantity.

Turning marker categories off

A long run crosses many joints, and on a chart zoomed to fifty legs the cluster of markers can be exactly what is in the way. So the key under the chart is also the switch: each category is a button carrying its own shape, its word and its marker count.

  • pressing a category stops it being drawn on the chart, on the strip under the chart, and in the register — one list, three readers, so the three views of a marker cannot disagree about whether it is drawn;
  • the key swatch is drawn from the same shape table the chart draws with, so the shape in the key IS the shape on the line;
  • a switched-off category keeps its shape and its colour, dimmed with its word struck through. A kind read as "off" by hue alone would be invisible to a reader who cannot separate the hues — which is the whole reason every kind has a shape.

Nothing is hidden silently. The register always states both counts — how many markers are drawn out of how many the snapshot carries — and adds a sentence naming how many your switches are holding back. The chart's accessible name carries the same numbers. That distinction is the point: a chart missing its joints looks exactly like a run with no joints, and the two call for opposite readings of the same curve.

A category the tab has never seen is offered as a switch under its own name and is drawn by default: the switches store the kinds turned off, so a producer's new kind, or a marker served with no kind, can never be hidden by a preference expressed before it existed. The switches are remembered per session and survive the pane remounting the tab body, for the same reason the series choice does: a switch that resets itself on the next poll undoes your work while looking exactly like a refresh.

Breaks and kinds

A long run crosses boundaries that are not learning, and a rise across one of them is not the model getting worse. The snapshot can carry them, and the tab draws each one as a vertical line carrying a shape and its leg number: the shape says which kind of joint it is, the number says which register row below reads it out, and the words stay in the register where there is room for them and where the reason, the meaning and the reading already live. Kinds are shapes and colours, because a kind encoded by hue alone is invisible to a reader who cannot separate the hues, and these kinds do different things to the curve:

kindshapewhat it means on the chart
corpuscirclethe data changed, so a rise here is expected and is not a regression
instrumentsquarethe reading changed, so the joint is a level shift measured on different terms: no slope across it
arithmetictrianglethe numbers changed, so the series is not comparable across the joint at all
shapediamondthe leg's geometry changed with its volume held constant, so the metric stays comparable and the wall clock moves
restarttriangle, point downthe process restarted, and the work of that window is not in the series
rollbackchevron, pointing leftthe weights were rewound to an earlier state, so the leg numbers either side of the joint do not order the training and the reading jumps for a reason that is not the model
growthchevron, pointing upthe MODEL grew: rows were added to the read, so the size steps at this leg, the reading can rise while the new rows learn, and the leg costs more wall clock. Marked on every chart, so an event the selected series does not move is still visible on it. The one kind that is not a warning
anything elseplusa kind this tab has never seen, drawn rather than hidden

A rollback is the one kind the chart draws twice, and the one kind that re-bases the x axis. Everything at or before the newest rollback's leg is the chain the controller threw away: it stays on the chart - dropping those points would make the run look shorter than it is - and it is drawn as a ghost: same hue, dashed, half opacity. Both passes are drawn from their own first leg, so the live pass starts at x=0 beside the older one instead of being stranded at the far right of an axis that starts at the run's first leg: the reader compares pass 2's third leg with pass 1's third leg, which is the whole reason the older values are kept on the picture. The window readout says so - legs from each pass's start 0–208 - because numbers that no longer match the legs in the register must not be left to be inferred. A run that has not rolled back keeps the leg number as its x, and its readout still says steps.

The key carries a third switch for the ghost, OLDER RUN ≤N, which takes the ghost off the picture and puts it back; neither axis moves either way - the horizontal one is bounded by both passes and the vertical by both passes' values - so switching the ghost off is a change of what is drawn and never of what the numbers mean. The ghost is read from the rollback MARKERS and not from the visible ones, so switching the ROLLBACK category off hides the marker and leaves the discarded values ghosted where they were.

A rollback draws no line of its own, because with both passes aligned to a shared origin there is no leg for such a line to sit at: it would land at the far end of the older chain and mark nothing. What shows it on the chart is the origin itself - two chains from one x, the older one ghosted - and the ROLLBACK row in the register carries the record: the rule, the checkpoint it went back to, and the producer's own reason. Its category switch therefore hides the RECORD, and says so rather than promising to take a line off a chart it is not drawn on.

A kind the tab has never seen is still drawn, with the plus shape and the producer's own name in the register; a marker whose kind the snapshot omits gets the UNKNOWN row rather than being shown as if the kind were known. Markers whose labels would collide are stacked so that no two write over each other, and every marker also gets a dim tick on the strip under the chart, so the joints are visible even when you have zoomed somewhere else. Hovering a marker gives the same text the register carries, so the detail is one gesture away without being written across the plot.

The register under the chart is a STATUS LIST, one line per joint: the shape and colour of its kind, the kind's word, the leg, and what changed there — with the reasoning behind a why this joint is marked disclosure on the row itself. The reason, the sentence your snapshot carries for that kind, the evidence the marker was read from and any contradiction it attaches are all still there, one press away: a register that arrives as four paragraphs per joint is not a status list and gets read as a wall — reported on 10 October 2026 and fixed by the disclosure. Markers that fall outside the window on screen, or past the last point of the series, still appear there — the chart can only draw what its axis covers, and a marker that is missing from the picture without a word about it is a marker nobody finds.

**Every marker comes from your snapshot, in y