-
Notifications
You must be signed in to change notification settings - Fork 15
fix(studio-app): compact loss chart points instead of tail-slicing (#215) #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 17 commits
ec7882d
c862527
c7134bf
94cf5e3
7175375
3f22473
20de904
3dab35e
46e674b
37dfceb
6714fe2
724217e
374341d
b1c1e55
43a1dc3
5207348
f3ca4b9
e2fd994
a0497b0
6fc02d3
6652d36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,8 @@ The loss chart is an SVG plot drawn from `training.log` events. It uses min-max | |
|
|
||
| Hovering shows the nearest step and whichever of `loss` / `evalLoss` are present at that step (eval-only steps don't show a `loss` value, and vice-versa). The chart shows the `Waiting for training.log events…` placeholder until at least one event with a numeric `loss` or `evalLoss` arrives; `training.log` frames where both fields are null/omitted don't count. | ||
|
|
||
| The chart keeps at most 2,000 points per job. Once a run exceeds that, older points are compacted rather than dropped, so the shape of the whole run, including its start, stays visible at coarser resolution instead of the chart silently losing its earliest data. The first and last points, every point carrying a numeric `evalLoss`, and local loss spikes are prioritized to survive compaction. | ||
|
|
||
| ### Advanced metrics | ||
|
|
||
| The **Advanced** toggle in the chart's header reveals a per-series statistics panel. Each card reports: | ||
|
|
@@ -77,6 +79,8 @@ The **Advanced** toggle in the chart's header reveals a per-series statistics pa | |
|
|
||
| The eval card stays empty until a `training.log` event with a numeric `evalLoss` arrives. | ||
|
|
||
| Mean, standard deviation, variance, and the confidence interval are computed from every value the run has emitted, not just what the chart is currently displaying, so they stay accurate for the whole run regardless of how long it gets or how much the chart has compacted. p90 and p95 are estimated from a bounded representative sample (up to 2,000 values per series) of the full run rather than every value exactly, since retaining every value indefinitely isn't practical for a long-running job; this sample is independent of the points the chart itself displays. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When users open Advanced metrics, the page still warns that stats describe only the retained chart sample, contradicting this new whole-run guarantee. Update the rendered panel copy and its stale comment so the UI matches the documented full-run statistics. Prompt for AI agents |
||
|
|
||
| ## Things this page does not do | ||
|
|
||
| - **No cancel button.** To stop a running job, call [`trainer.cancel()`](/sdk/trainer-control#cancel) from your own code that drives the trainer. Studio does not expose this in the UI today. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react"; | ||
|
|
||
| import { summarize, type LossStats } from "../../lib/stats"; | ||
| import { | ||
| summarize, | ||
| finalizeRunningStats, | ||
| type LossStats, | ||
| type RunningStats, | ||
| } from "../../lib/stats"; | ||
|
|
||
| export interface LossPoint { | ||
| step: number; | ||
|
|
@@ -27,9 +32,22 @@ const EVAL_STROKE = "rgb(244 114 182)"; // pink-400 | |
| export function LossChart({ | ||
| points, | ||
| advanced = false, | ||
| trainRunning, | ||
| evalRunning, | ||
| }: { | ||
| points: LossPoint[]; | ||
| advanced?: boolean; | ||
| /** | ||
| * Optional full-run stats accumulators (see stats.ts), computed | ||
| * incrementally by the caller independent of any compaction applied | ||
| * to `points`. When provided, these are used instead of deriving | ||
| * stats from `points` directly, so the Advanced panel stays | ||
| * accurate for the whole run even once `points` has been compacted. | ||
| * Falls back to summarizing `points` when omitted (e.g. for callers | ||
| * without a persistent per-run accumulator). | ||
| */ | ||
| trainRunning?: RunningStats | null; | ||
| evalRunning?: RunningStats | null; | ||
| }) { | ||
| const wrapperRef = useRef<HTMLDivElement>(null); | ||
| const [width, setWidth] = useState(640); | ||
|
|
@@ -126,20 +144,24 @@ export function LossChart({ | |
| // baked into `summarize()` (for percentiles) doesn't run during a | ||
| // live training stream when the panel isn't visible. Toggling | ||
| // `advanced` on triggers a fresh useMemo evaluation. | ||
| const trainStats = useMemo( | ||
| () => | ||
| advanced && trainSeries.length > 0 | ||
| ? summarize(trainSeries.map((p) => p.loss)) | ||
| : null, | ||
| [advanced, trainSeries], | ||
| ); | ||
| const evalStats = useMemo( | ||
| () => | ||
| advanced && evalSeries.length > 0 | ||
| ? summarize(evalSeries.map((p) => p.evalLoss)) | ||
| : null, | ||
| [advanced, evalSeries], | ||
| ); | ||
| const trainStats = useMemo(() => { | ||
| if (!advanced) return null; | ||
| if (trainRunning) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When navigating to a different job, the Advanced stats can show the previous job’s aggregates because this branch always prefers Prompt for AI agents |
||
| return trainRunning.count > 0 ? finalizeRunningStats(trainRunning) : null; | ||
| } | ||
| return trainSeries.length > 0 | ||
| ? summarize(trainSeries.map((p) => p.loss)) | ||
| : null; | ||
| }, [advanced, trainRunning, trainSeries]); | ||
| const evalStats = useMemo(() => { | ||
| if (!advanced) return null; | ||
| if (evalRunning) { | ||
| return evalRunning.count > 0 ? finalizeRunningStats(evalRunning) : null; | ||
| } | ||
| return evalSeries.length > 0 | ||
| ? summarize(evalSeries.map((p) => p.evalLoss)) | ||
| : null; | ||
| }, [advanced, evalRunning, evalSeries]); | ||
|
|
||
| if (unified.length === 0) { | ||
| return ( | ||
|
|
@@ -437,7 +459,12 @@ export function LossChart({ | |
| ) : null} | ||
|
|
||
| {advanced ? ( | ||
| <AdvancedStats train={trainStats} evalStats={evalStats} /> | ||
| <AdvancedStats | ||
| train={trainStats} | ||
| evalStats={evalStats} | ||
| trainUsingFullRunStats={Boolean(trainRunning)} | ||
| evalUsingFullRunStats={Boolean(evalRunning)} | ||
| /> | ||
| ) : null} | ||
| </div> | ||
| ); | ||
|
|
@@ -481,19 +508,54 @@ function Legend({ | |
| function AdvancedStats({ | ||
| train, | ||
| evalStats, | ||
| trainUsingFullRunStats, | ||
| evalUsingFullRunStats, | ||
| }: { | ||
| train: LossStats | null; | ||
| evalStats: LossStats | null; | ||
| /** | ||
| * True when the caller supplied a `trainRunning` / `evalRunning` | ||
| * accumulator (see LossChart's props) for that specific series, so | ||
| * its stats above came from `finalizeRunningStats` rather than | ||
| * `summarize()` over `points`. Tracked per series (not combined) | ||
| * since a caller could in principle supply one accumulator without | ||
| * the other, and only the series with an accumulator has exact | ||
| * mean/variance/CI for the whole run; the other still describes | ||
| * whatever `points` currently holds. | ||
| */ | ||
| trainUsingFullRunStats: boolean; | ||
| evalUsingFullRunStats: boolean; | ||
| }) { | ||
| const fullRunCaption = | ||
| "Mean, standard deviation, variance, and the confidence interval reflect the full run. p90/p95 are estimated from a representative sample of up to 2,000 values, since retaining every value indefinitely isn't practical for a long run."; | ||
| const retainedSampleCaption = | ||
| "Stats describe the currently retained sample, not necessarily every point ever emitted for a long run."; | ||
| let caption: string; | ||
| if (trainUsingFullRunStats && evalUsingFullRunStats) { | ||
| caption = fullRunCaption; | ||
| } else if (!trainUsingFullRunStats && !evalUsingFullRunStats) { | ||
| caption = retainedSampleCaption; | ||
| } else { | ||
| const fullRunLabel = trainUsingFullRunStats ? "Training loss" : "Eval loss"; | ||
| const retainedLabel = trainUsingFullRunStats | ||
| ? "Eval loss" | ||
| : "Training loss"; | ||
| caption = `${fullRunLabel} stats reflect the full run (p90/p95 estimated from a representative sample). ${retainedLabel} stats describe the currently retained sample.`; | ||
| } | ||
| return ( | ||
| <div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2"> | ||
| <StatsCard label="Training loss" tone="train" stats={train} /> | ||
| <StatsCard | ||
| label="Eval loss" | ||
| tone="eval" | ||
| stats={evalStats} | ||
| emptyHint="Awaiting training.log events with evalLoss…" | ||
| /> | ||
| <div className="mt-4"> | ||
| <p className="mb-2 text-[10px] text-zinc-500 dark:text-zinc-400"> | ||
| {caption} | ||
| </p> | ||
| <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> | ||
| <StatsCard label="Training loss" tone="train" stats={train} /> | ||
| <StatsCard | ||
| label="Eval loss" | ||
| tone="eval" | ||
| stats={evalStats} | ||
| emptyHint="Awaiting training.log events with evalLoss…" | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the exact retention terminology in both documentation files. The implementation retains finite numeric
evalLossvalues and preserves local minima and maxima, not only upward spikes.docs/studio/jobs.mdx#L70-L71: replace “numericevalLoss” with “finite numericevalLoss” and “local loss spikes” with “local loss minima and maxima”.docs/ja/studio/jobs.mdx#L70-L71: replace数値の evalLosswith有限な数値の evalLossand局所的な loss のスパイクwithloss の局所的な極小値と極大値.📍 Affects 2 files
docs/studio/jobs.mdx#L70-L71(this comment)docs/ja/studio/jobs.mdx#L70-L71🤖 Prompt for AI Agents