feat: add reusable chart components for training metrics visualization
- Introduced `EvalLossChartCard`, `GradNormChartCard`, `LearningRateChartCard`, and `TrainingLossChartCard` components. - Implemented shared chart settings via `SharedChartSettings` to manage scale, outliers, and view configuration. - Added utilities for metrics formatting, step tick generation, data compression, and smoothing (`utils.ts`). - Created types and structures for chart data handling (`types.ts`).
This commit is contained in:
parent
0be3e6f525
commit
2d90210a69
9 changed files with 1156 additions and 636 deletions
|
|
@ -1,151 +1,19 @@
|
|||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { ChartAverageIcon, Settings02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ReactElement, useMemo, useState } from "react";
|
||||
import { EvalLossChartCard } from "./charts/eval-loss-chart-card";
|
||||
import { GradNormChartCard } from "./charts/grad-norm-chart-card";
|
||||
import { LearningRateChartCard } from "./charts/learning-rate-chart-card";
|
||||
import { TrainingLossChartCard } from "./charts/training-loss-chart-card";
|
||||
import type { OutlierMode, ScaleMode, TrainingChartSeries, ViewSettingsState } from "./charts/types";
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const lossConfig = {
|
||||
loss: { label: "Loss", color: "#3b82f6" },
|
||||
smoothed: { label: "Smoothed", color: "#f59e0b" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const lrConfig = {
|
||||
lr: { label: "LR", color: "#8b5cf6" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const gradNormConfig = {
|
||||
gradNorm: { label: "Grad Norm", color: "#f97316" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const evalLossConfig = {
|
||||
loss: { label: "Eval Loss", color: "#ef4444" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const placeholderEvalData = [
|
||||
{ step: 0, loss: 2.8 },
|
||||
{ step: 50, loss: 2.4 },
|
||||
{ step: 100, loss: 2.0 },
|
||||
{ step: 150, loss: 1.7 },
|
||||
{ step: 200, loss: 1.5 },
|
||||
];
|
||||
|
||||
type LossHistoryItem = { step: number; loss: number };
|
||||
type SmoothedLossItem = LossHistoryItem & { smoothed: number };
|
||||
|
||||
interface TrainingChartSeries {
|
||||
lossHistory: LossHistoryItem[];
|
||||
lrHistory: { step: number; lr: number }[];
|
||||
gradNormHistory: { step: number; gradNorm: number }[];
|
||||
evalLossHistory: { step: number; loss: number }[];
|
||||
}
|
||||
|
||||
const CHART_SYNC_ID = "train-metrics-sync";
|
||||
const MAX_RENDER_POINTS = 800;
|
||||
const DEFAULT_VISIBLE_POINTS = 160;
|
||||
|
||||
function formatStepTick(value: number): string {
|
||||
if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(1)}k`;
|
||||
}
|
||||
return String(Math.round(value));
|
||||
}
|
||||
|
||||
function compressSeries<T>(data: T[], maxPoints: number): T[] {
|
||||
if (data.length <= maxPoints) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const stride = Math.ceil(data.length / maxPoints);
|
||||
return data.filter(
|
||||
(_item, index) => index % stride === 0 || index === data.length - 1,
|
||||
);
|
||||
}
|
||||
|
||||
function buildStepTicks(min: number, max: number, targetCount = 6): number[] {
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
return [0, 1];
|
||||
}
|
||||
if (max <= min) {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
const stepSize = Math.max(1, Math.ceil((max - min) / (targetCount - 1)));
|
||||
const ticks: number[] = [];
|
||||
let current = min;
|
||||
|
||||
while (current < max) {
|
||||
ticks.push(current);
|
||||
current += stepSize;
|
||||
}
|
||||
|
||||
ticks.push(max);
|
||||
return Array.from(new Set(ticks));
|
||||
}
|
||||
|
||||
function buildYDomain(values: number[]): [number, number] {
|
||||
const finiteValues = values.filter((value) => Number.isFinite(value));
|
||||
if (finiteValues.length === 0) {
|
||||
return [0, 1];
|
||||
}
|
||||
|
||||
const min = Math.min(...finiteValues);
|
||||
const max = Math.max(...finiteValues);
|
||||
|
||||
if (min === max) {
|
||||
const base = Math.abs(min);
|
||||
const pad = base > 0 ? base * 0.08 : 0.1;
|
||||
return [min - pad, max + pad];
|
||||
}
|
||||
|
||||
const pad = (max - min) * 0.12;
|
||||
return [min - pad, max + pad];
|
||||
}
|
||||
|
||||
function ema(data: LossHistoryItem[], alpha: number): SmoothedLossItem[] {
|
||||
if (data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let s = data[0].loss;
|
||||
return data.map((d) => {
|
||||
s = alpha * d.loss + (1 - alpha) * s;
|
||||
return { ...d, smoothed: +s.toFixed(4) };
|
||||
});
|
||||
}
|
||||
DEFAULT_VISIBLE_POINTS,
|
||||
MAX_RENDER_POINTS,
|
||||
applyOutlierCap,
|
||||
buildStepTicks,
|
||||
buildYDomain,
|
||||
compressSeries,
|
||||
ema,
|
||||
toLog1p,
|
||||
} from "./charts/utils";
|
||||
|
||||
export function ChartsContent({
|
||||
metrics,
|
||||
|
|
@ -156,48 +24,65 @@ export function ChartsContent({
|
|||
const [showRaw, setShowRaw] = useState(true);
|
||||
const [showSmoothed, setShowSmoothed] = useState(true);
|
||||
const [showAvgLine, setShowAvgLine] = useState(true);
|
||||
const [windowSize, setWindowSize] = useState(DEFAULT_VISIBLE_POINTS);
|
||||
const [panOffset, setPanOffset] = useState(0);
|
||||
|
||||
const [lossScale, setLossScale] = useState<ScaleMode>("linear");
|
||||
const [lrScale, setLrScale] = useState<ScaleMode>("linear");
|
||||
const [gradScale, setGradScale] = useState<ScaleMode>("linear");
|
||||
|
||||
const [lossOutlierMode, setLossOutlierMode] = useState<OutlierMode>("none");
|
||||
const [gradOutlierMode, setGradOutlierMode] = useState<OutlierMode>("none");
|
||||
const [lrOutlierMode, setLrOutlierMode] = useState<OutlierMode>("none");
|
||||
|
||||
const lossHistory = metrics.lossHistory;
|
||||
const smoothedData = useMemo(
|
||||
() => (lossHistory.length > 0 ? ema(lossHistory, 1 - smoothing) : []),
|
||||
[lossHistory, smoothing],
|
||||
() => (metrics.lossHistory.length > 0 ? ema(metrics.lossHistory, 1 - smoothing) : []),
|
||||
[metrics.lossHistory, smoothing],
|
||||
);
|
||||
|
||||
const reducedLossData = useMemo(
|
||||
() => compressSeries(smoothedData, MAX_RENDER_POINTS),
|
||||
[smoothedData],
|
||||
);
|
||||
|
||||
const reducedGradNormData = useMemo(
|
||||
() => compressSeries(metrics.gradNormHistory, MAX_RENDER_POINTS),
|
||||
[metrics.gradNormHistory],
|
||||
);
|
||||
|
||||
const reducedLrData = useMemo(
|
||||
() => compressSeries(metrics.lrHistory, MAX_RENDER_POINTS),
|
||||
[metrics.lrHistory],
|
||||
);
|
||||
|
||||
const reducedEvalLossData = useMemo(
|
||||
() => compressSeries(metrics.evalLossHistory, MAX_RENDER_POINTS),
|
||||
[metrics.evalLossHistory],
|
||||
);
|
||||
|
||||
const visibleStepDomain = useMemo<[number, number]>(() => {
|
||||
const allSteps = [
|
||||
...reducedLossData.map((point) => point.step),
|
||||
...reducedGradNormData.map((point) => point.step),
|
||||
...reducedLrData.map((point) => point.step),
|
||||
].sort((a, b) => a - b);
|
||||
const allSteps = useMemo(() => {
|
||||
const set = new Set<number>();
|
||||
for (const point of reducedLossData) set.add(point.step);
|
||||
for (const point of reducedGradNormData) set.add(point.step);
|
||||
for (const point of reducedLrData) set.add(point.step);
|
||||
return Array.from(set).sort((a, b) => a - b);
|
||||
}, [reducedGradNormData, reducedLossData, reducedLrData]);
|
||||
|
||||
const effectiveWindowSize = Math.min(
|
||||
Math.max(1, Math.round(windowSize)),
|
||||
Math.max(1, allSteps.length),
|
||||
);
|
||||
const maxPanOffset = Math.max(0, allSteps.length - effectiveWindowSize);
|
||||
const effectivePanOffset = Math.min(Math.max(0, Math.round(panOffset)), maxPanOffset);
|
||||
|
||||
const visibleStepDomain = useMemo<[number, number]>(() => {
|
||||
if (allSteps.length === 0) {
|
||||
return [0, 1];
|
||||
}
|
||||
|
||||
const endIndex = Math.max(0, allSteps.length - 1 - effectivePanOffset);
|
||||
const startIndex = Math.max(0, endIndex - effectiveWindowSize + 1);
|
||||
const minStep = allSteps[0] ?? 0;
|
||||
const endStep = allSteps[allSteps.length - 1] ?? 1;
|
||||
const startIndex = Math.max(0, allSteps.length - DEFAULT_VISIBLE_POINTS);
|
||||
const startStep = allSteps[startIndex] ?? minStep;
|
||||
const endStep = allSteps[endIndex] ?? startStep;
|
||||
|
||||
if (startStep === endStep) {
|
||||
return [startStep, startStep + 4];
|
||||
}
|
||||
|
|
@ -205,68 +90,114 @@ export function ChartsContent({
|
|||
return [Math.max(minStep, endStep - 6), endStep];
|
||||
}
|
||||
return [startStep, endStep];
|
||||
}, [reducedGradNormData, reducedLossData, reducedLrData]);
|
||||
}, [allSteps, effectivePanOffset, effectiveWindowSize]);
|
||||
|
||||
const xAxisTicks = useMemo(
|
||||
() => buildStepTicks(visibleStepDomain[0], visibleStepDomain[1]),
|
||||
[visibleStepDomain],
|
||||
);
|
||||
|
||||
const visibleLossValues = useMemo(
|
||||
const displayLossData = useMemo(
|
||||
() =>
|
||||
reducedLossData
|
||||
.filter(
|
||||
(point) =>
|
||||
point.step >= visibleStepDomain[0] && point.step <= visibleStepDomain[1],
|
||||
)
|
||||
.map((point) => point.loss),
|
||||
[reducedLossData, visibleStepDomain],
|
||||
reducedLossData.map((point) => ({
|
||||
...point,
|
||||
displayLoss: lossScale === "log" ? toLog1p(point.loss) : point.loss,
|
||||
displaySmoothed:
|
||||
lossScale === "log" ? toLog1p(point.smoothed) : point.smoothed,
|
||||
})),
|
||||
[lossScale, reducedLossData],
|
||||
);
|
||||
|
||||
const visibleSmoothValues = useMemo(
|
||||
const displayGradData = useMemo(
|
||||
() =>
|
||||
reducedLossData
|
||||
.filter(
|
||||
(point) =>
|
||||
point.step >= visibleStepDomain[0] && point.step <= visibleStepDomain[1],
|
||||
)
|
||||
.map((point) => point.smoothed),
|
||||
[reducedLossData, visibleStepDomain],
|
||||
reducedGradNormData.map((point) => ({
|
||||
...point,
|
||||
displayGradNorm:
|
||||
gradScale === "log" ? toLog1p(point.gradNorm) : point.gradNorm,
|
||||
})),
|
||||
[gradScale, reducedGradNormData],
|
||||
);
|
||||
|
||||
const visibleGradValues = useMemo(
|
||||
const displayLrData = useMemo(
|
||||
() =>
|
||||
reducedGradNormData
|
||||
reducedLrData.map((point) => ({
|
||||
...point,
|
||||
displayLr: lrScale === "log" ? toLog1p(point.lr) : point.lr,
|
||||
})),
|
||||
[lrScale, reducedLrData],
|
||||
);
|
||||
|
||||
const visibleLossDisplayValues = useMemo(() => {
|
||||
const values: number[] = [];
|
||||
|
||||
for (const point of displayLossData) {
|
||||
if (point.step < visibleStepDomain[0] || point.step > visibleStepDomain[1]) {
|
||||
continue;
|
||||
}
|
||||
if (showRaw && Number.isFinite(point.displayLoss)) {
|
||||
values.push(point.displayLoss);
|
||||
}
|
||||
if (showSmoothed && Number.isFinite(point.displaySmoothed)) {
|
||||
values.push(point.displaySmoothed);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
for (const point of displayLossData) {
|
||||
if (point.step < visibleStepDomain[0] || point.step > visibleStepDomain[1]) {
|
||||
continue;
|
||||
}
|
||||
if (Number.isFinite(point.displayLoss)) {
|
||||
values.push(point.displayLoss);
|
||||
}
|
||||
if (Number.isFinite(point.displaySmoothed)) {
|
||||
values.push(point.displaySmoothed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}, [displayLossData, showRaw, showSmoothed, visibleStepDomain]);
|
||||
|
||||
const visibleGradDisplayValues = useMemo(
|
||||
() =>
|
||||
displayGradData
|
||||
.filter(
|
||||
(point) =>
|
||||
point.step >= visibleStepDomain[0] && point.step <= visibleStepDomain[1],
|
||||
)
|
||||
.map((point) => point.gradNorm)
|
||||
.map((point) => point.displayGradNorm)
|
||||
.filter((value) => Number.isFinite(value)),
|
||||
[reducedGradNormData, visibleStepDomain],
|
||||
[displayGradData, visibleStepDomain],
|
||||
);
|
||||
|
||||
const visibleLrValues = useMemo(
|
||||
const visibleLrDisplayValues = useMemo(
|
||||
() =>
|
||||
reducedLrData
|
||||
displayLrData
|
||||
.filter(
|
||||
(point) =>
|
||||
point.step >= visibleStepDomain[0] && point.step <= visibleStepDomain[1],
|
||||
)
|
||||
.map((point) => point.lr)
|
||||
.map((point) => point.displayLr)
|
||||
.filter((value) => Number.isFinite(value)),
|
||||
[reducedLrData, visibleStepDomain],
|
||||
[displayLrData, visibleStepDomain],
|
||||
);
|
||||
|
||||
const lossDomain = useMemo(
|
||||
() => buildYDomain([...visibleLossValues, ...visibleSmoothValues]),
|
||||
[visibleLossValues, visibleSmoothValues],
|
||||
() => buildYDomain(applyOutlierCap(visibleLossDisplayValues, lossOutlierMode)),
|
||||
[lossOutlierMode, visibleLossDisplayValues],
|
||||
);
|
||||
const gradDomain = useMemo(
|
||||
() => buildYDomain(applyOutlierCap(visibleGradDisplayValues, gradOutlierMode)),
|
||||
[gradOutlierMode, visibleGradDisplayValues],
|
||||
);
|
||||
const lrDomain = useMemo(
|
||||
() => buildYDomain(applyOutlierCap(visibleLrDisplayValues, lrOutlierMode)),
|
||||
[lrOutlierMode, visibleLrDisplayValues],
|
||||
);
|
||||
const gradDomain = useMemo(() => buildYDomain(visibleGradValues), [visibleGradValues]);
|
||||
const lrDomain = useMemo(() => buildYDomain(visibleLrValues), [visibleLrValues]);
|
||||
|
||||
const evalLossDomain = useMemo(() => {
|
||||
const vals = reducedEvalLossData.map((p) => p.loss);
|
||||
const vals = reducedEvalLossData.map((point) => point.loss);
|
||||
return buildYDomain(vals);
|
||||
}, [reducedEvalLossData]);
|
||||
|
||||
|
|
@ -277,427 +208,78 @@ export function ChartsContent({
|
|||
return buildStepTicks(min, max);
|
||||
}, [reducedEvalLossData]);
|
||||
|
||||
const avg =
|
||||
const avgRaw =
|
||||
metrics.lossHistory.length > 0
|
||||
? +(
|
||||
metrics.lossHistory.reduce((a, b) => a + b.loss, 0) /
|
||||
metrics.lossHistory.length
|
||||
).toFixed(4)
|
||||
metrics.lossHistory.reduce((sum, point) => sum + point.loss, 0) /
|
||||
metrics.lossHistory.length
|
||||
).toFixed(4)
|
||||
: 0;
|
||||
const avgDisplay = lossScale === "log" ? toLog1p(avgRaw) : avgRaw;
|
||||
|
||||
const minWindow = Math.min(10, Math.max(1, allSteps.length));
|
||||
const viewSettings: ViewSettingsState = {
|
||||
effectiveWindowSize,
|
||||
minWindow,
|
||||
allStepsLength: allSteps.length,
|
||||
effectivePanOffset,
|
||||
maxPanOffset,
|
||||
setWindowSize: (value) => setWindowSize(value),
|
||||
setPanOffset: (value) => setPanOffset(value),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Card data-tour="studio-training-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Training Loss</CardTitle>
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={Settings02Icon} className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-xs">
|
||||
Chart Settings
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex flex-col gap-1.5 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">Smoothing</Label>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{smoothing.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[smoothing]}
|
||||
onValueChange={([v]) => setSmoothing(v)}
|
||||
min={0}
|
||||
max={0.99}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showRaw}
|
||||
onCheckedChange={setShowRaw}
|
||||
>
|
||||
Show raw loss
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showSmoothed}
|
||||
onCheckedChange={setShowSmoothed}
|
||||
>
|
||||
Show smoothed loss
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showAvgLine}
|
||||
onCheckedChange={setShowAvgLine}
|
||||
>
|
||||
Show average line
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={lossConfig} className="-ml-3 h-[220px] w-full">
|
||||
<LineChart
|
||||
data={reducedLossData}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={lossDomain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{showAvgLine && (
|
||||
<ReferenceLine
|
||||
y={avg}
|
||||
stroke="#3b82f6"
|
||||
strokeDasharray="4 4"
|
||||
strokeOpacity={0.5}
|
||||
label={{
|
||||
value: `avg ${avg}`,
|
||||
position: "insideTopRight",
|
||||
fontSize: 10,
|
||||
fill: "#3b82f6",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRaw && (
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={1.2}
|
||||
strokeOpacity={showSmoothed ? 0.35 : 1}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)}
|
||||
{showSmoothed && (
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="smoothed"
|
||||
stroke="var(--color-smoothed)"
|
||||
strokeWidth={2.2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)}
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Gradient Norm</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer
|
||||
config={gradNormConfig}
|
||||
className="-ml-3 h-[220px] w-full"
|
||||
>
|
||||
<LineChart
|
||||
data={reducedGradNormData}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={gradDomain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="gradNorm"
|
||||
stroke="var(--color-gradNorm)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Learning Rate</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={lrConfig} className="-ml-1.5 h-[220px] w-full">
|
||||
<LineChart
|
||||
data={reducedLrData}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={lrDomain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num.toExponential(0) : "0e+0";
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
formatter={(value) => {
|
||||
const num = Number(value);
|
||||
return [Number.isFinite(num) ? num.toExponential(3) : "0e+0", "LR"];
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="lr"
|
||||
stroke="var(--color-lr)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card data-tour="studio-eval-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className={`text-sm pl-2${reducedEvalLossData.length > 0 ? "" : " text-muted-foreground"}`}>
|
||||
Eval Loss
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{reducedEvalLossData.length > 0 ? (
|
||||
<ChartContainer
|
||||
config={evalLossConfig}
|
||||
className="-ml-3 h-[220px] w-full"
|
||||
>
|
||||
<LineChart
|
||||
data={reducedEvalLossData}
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
ticks={evalLossStepTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={evalLossDomain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, strokeWidth: 0, fill: "#ef4444" }}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<ChartContainer
|
||||
config={evalLossConfig}
|
||||
className="-ml-3 h-[220px] w-full blur"
|
||||
>
|
||||
<LineChart
|
||||
data={placeholderEvalData}
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={40}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1">
|
||||
<HugeiconsIcon
|
||||
icon={ChartAverageIcon}
|
||||
className="size-5 text-muted-foreground/50"
|
||||
/>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{isTraining && evalEnabled ? "Waiting for first evaluation step…" : "Evaluation not configured"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
{isTraining && evalEnabled ? "Chart will appear once eval_steps is reached" : "Set eval dataset & eval_steps to track eval loss"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TrainingLossChartCard
|
||||
data={displayLossData}
|
||||
domain={lossDomain}
|
||||
visibleStepDomain={visibleStepDomain}
|
||||
xAxisTicks={xAxisTicks}
|
||||
avgRaw={avgRaw}
|
||||
avgDisplay={avgDisplay}
|
||||
smoothing={smoothing}
|
||||
setSmoothing={setSmoothing}
|
||||
showRaw={showRaw}
|
||||
setShowRaw={setShowRaw}
|
||||
showSmoothed={showSmoothed}
|
||||
setShowSmoothed={setShowSmoothed}
|
||||
showAvgLine={showAvgLine}
|
||||
setShowAvgLine={setShowAvgLine}
|
||||
viewSettings={viewSettings}
|
||||
scale={lossScale}
|
||||
setScale={setLossScale}
|
||||
outlierMode={lossOutlierMode}
|
||||
setOutlierMode={setLossOutlierMode}
|
||||
/>
|
||||
<GradNormChartCard
|
||||
data={displayGradData}
|
||||
domain={gradDomain}
|
||||
visibleStepDomain={visibleStepDomain}
|
||||
xAxisTicks={xAxisTicks}
|
||||
scale={gradScale}
|
||||
setScale={setGradScale}
|
||||
outlierMode={gradOutlierMode}
|
||||
setOutlierMode={setGradOutlierMode}
|
||||
viewSettings={viewSettings}
|
||||
/>
|
||||
<LearningRateChartCard
|
||||
data={displayLrData}
|
||||
domain={lrDomain}
|
||||
visibleStepDomain={visibleStepDomain}
|
||||
xAxisTicks={xAxisTicks}
|
||||
scale={lrScale}
|
||||
setScale={setLrScale}
|
||||
outlierMode={lrOutlierMode}
|
||||
setOutlierMode={setLrOutlierMode}
|
||||
viewSettings={viewSettings}
|
||||
/>
|
||||
<EvalLossChartCard
|
||||
data={reducedEvalLossData}
|
||||
domain={evalLossDomain}
|
||||
ticks={evalLossStepTicks}
|
||||
isTraining={isTraining}
|
||||
evalEnabled={evalEnabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import { ChartAverageIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import { formatStepTick, placeholderEvalData } from "./utils";
|
||||
|
||||
const evalLossConfig = {
|
||||
loss: { label: "Eval Loss", color: "#ef4444" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function EvalLossChartCard({
|
||||
data,
|
||||
domain,
|
||||
ticks,
|
||||
isTraining,
|
||||
evalEnabled,
|
||||
}: {
|
||||
data: { step: number; loss: number }[];
|
||||
domain: [number, number];
|
||||
ticks?: number[];
|
||||
isTraining: boolean;
|
||||
evalEnabled: boolean;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Card data-tour="studio-eval-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className={`text-sm pl-2${data.length > 0 ? "" : " text-muted-foreground"}`}>
|
||||
Eval Loss
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.length > 0 ? (
|
||||
<ChartContainer config={evalLossConfig} className="-ml-3 h-[220px] w-full">
|
||||
<LineChart data={data} accessibilityLayer={true} margin={{ left: 0, right: 8 }}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
ticks={ticks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={domain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, strokeWidth: 0, fill: "#ef4444" }}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<ChartContainer config={evalLossConfig} className="-ml-3 h-[220px] w-full blur">
|
||||
<LineChart
|
||||
data={placeholderEvalData}
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={40}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="loss"
|
||||
stroke="var(--color-loss)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1">
|
||||
<HugeiconsIcon icon={ChartAverageIcon} className="size-5 text-muted-foreground/50" />
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{isTraining && evalEnabled
|
||||
? "Waiting for first evaluation step…"
|
||||
: "Evaluation not configured"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
{isTraining && evalEnabled
|
||||
? "Chart will appear once eval_steps is reached"
|
||||
: "Set eval dataset & eval_steps to track eval loss"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Settings02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import { SharedChartSettings } from "./shared-chart-settings";
|
||||
import type { OutlierMode, ScaleMode, ViewSettingsState } from "./types";
|
||||
import { CHART_SYNC_ID, formatMetric, formatStepTick, fromLog1p } from "./utils";
|
||||
|
||||
const gradNormConfig = {
|
||||
displayGradNorm: { label: "Grad Norm", color: "#f97316" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
interface GradNormPoint {
|
||||
step: number;
|
||||
gradNorm: number;
|
||||
displayGradNorm: number;
|
||||
}
|
||||
|
||||
export function GradNormChartCard({
|
||||
data,
|
||||
domain,
|
||||
visibleStepDomain,
|
||||
xAxisTicks,
|
||||
scale,
|
||||
setScale,
|
||||
outlierMode,
|
||||
setOutlierMode,
|
||||
viewSettings,
|
||||
}: {
|
||||
data: GradNormPoint[];
|
||||
domain: [number, number];
|
||||
visibleStepDomain: [number, number];
|
||||
xAxisTicks: number[];
|
||||
scale: ScaleMode;
|
||||
setScale: (value: ScaleMode) => void;
|
||||
outlierMode: OutlierMode;
|
||||
setOutlierMode: (value: OutlierMode) => void;
|
||||
viewSettings: ViewSettingsState;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Gradient Norm</CardTitle>
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={Settings02Icon} className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-xs">Chart Settings</DropdownMenuLabel>
|
||||
<SharedChartSettings
|
||||
view={viewSettings}
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
outlierMode={outlierMode}
|
||||
setOutlierMode={setOutlierMode}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={gradNormConfig} className="-ml-3 h-[220px] w-full">
|
||||
<LineChart
|
||||
data={data}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={domain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return "0";
|
||||
const shown = scale === "log" ? fromLog1p(num) : num;
|
||||
return formatMetric(shown);
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
formatter={(_value, _name, item) => {
|
||||
const raw = Number(item?.payload?.gradNorm);
|
||||
return [formatMetric(raw), "Grad Norm"];
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="displayGradNorm"
|
||||
stroke="var(--color-displayGradNorm)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Settings02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
import { SharedChartSettings } from "./shared-chart-settings";
|
||||
import type { OutlierMode, ScaleMode, ViewSettingsState } from "./types";
|
||||
import { CHART_SYNC_ID, formatStepTick, fromLog1p } from "./utils";
|
||||
|
||||
const lrConfig = {
|
||||
displayLr: { label: "LR", color: "#8b5cf6" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
interface LearningRatePoint {
|
||||
step: number;
|
||||
lr: number;
|
||||
displayLr: number;
|
||||
}
|
||||
|
||||
export function LearningRateChartCard({
|
||||
data,
|
||||
domain,
|
||||
visibleStepDomain,
|
||||
xAxisTicks,
|
||||
scale,
|
||||
setScale,
|
||||
outlierMode,
|
||||
setOutlierMode,
|
||||
viewSettings,
|
||||
}: {
|
||||
data: LearningRatePoint[];
|
||||
domain: [number, number];
|
||||
visibleStepDomain: [number, number];
|
||||
xAxisTicks: number[];
|
||||
scale: ScaleMode;
|
||||
setScale: (value: ScaleMode) => void;
|
||||
outlierMode: OutlierMode;
|
||||
setOutlierMode: (value: OutlierMode) => void;
|
||||
viewSettings: ViewSettingsState;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Learning Rate</CardTitle>
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={Settings02Icon} className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-xs">Chart Settings</DropdownMenuLabel>
|
||||
<SharedChartSettings
|
||||
view={viewSettings}
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
outlierMode={outlierMode}
|
||||
setOutlierMode={setOutlierMode}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={lrConfig} className="-ml-1.5 h-[220px] w-full">
|
||||
<LineChart
|
||||
data={data}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={domain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return "0e+0";
|
||||
const shown = scale === "log" ? fromLog1p(num) : num;
|
||||
return shown.toExponential(0);
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
formatter={(_value, _name, item) => {
|
||||
const raw = Number(item?.payload?.lr);
|
||||
return [
|
||||
Number.isFinite(raw) ? raw.toExponential(3) : "0e+0",
|
||||
"LR",
|
||||
];
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="displayLr"
|
||||
stroke="var(--color-displayLr)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import type { ReactElement } from "react";
|
||||
import type { OutlierMode, ScaleMode, ViewSettingsState } from "./types";
|
||||
|
||||
export function SharedChartSettings({
|
||||
view,
|
||||
scale,
|
||||
setScale,
|
||||
outlierMode,
|
||||
setOutlierMode,
|
||||
}: {
|
||||
view: ViewSettingsState;
|
||||
scale: ScaleMode;
|
||||
setScale: (value: ScaleMode) => void;
|
||||
outlierMode: OutlierMode;
|
||||
setOutlierMode: (value: OutlierMode) => void;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs">View</DropdownMenuLabel>
|
||||
<div className="flex flex-col gap-1.5 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">Window (steps)</Label>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{view.effectiveWindowSize}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[view.effectiveWindowSize]}
|
||||
onValueChange={([v]) => view.setWindowSize(Math.max(1, Math.round(v)))}
|
||||
min={view.minWindow}
|
||||
max={Math.max(view.minWindow, view.allStepsLength)}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">Pan</Label>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{view.effectivePanOffset}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[view.effectivePanOffset]}
|
||||
onValueChange={([v]) => view.setPanOffset(Math.max(0, Math.round(v)))}
|
||||
min={0}
|
||||
max={Math.max(0, view.maxPanOffset)}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs">Y Scale</DropdownMenuLabel>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={scale === "linear"}
|
||||
onCheckedChange={(checked) => checked && setScale("linear")}
|
||||
>
|
||||
Linear
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={scale === "log"}
|
||||
onCheckedChange={(checked) => checked && setScale("log")}
|
||||
>
|
||||
Log (log1p)
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs">Outliers</DropdownMenuLabel>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={outlierMode === "none"}
|
||||
onCheckedChange={(checked) => checked && setOutlierMode("none")}
|
||||
>
|
||||
No clipping
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={outlierMode === "p99"}
|
||||
onCheckedChange={(checked) => checked && setOutlierMode("p99")}
|
||||
>
|
||||
Clip above p99
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={outlierMode === "p95"}
|
||||
onCheckedChange={(checked) => checked && setOutlierMode("p95")}
|
||||
>
|
||||
Clip above p95
|
||||
</DropdownMenuCheckboxItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Settings02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { CartesianGrid, Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";
|
||||
import { SharedChartSettings } from "./shared-chart-settings";
|
||||
import type { OutlierMode, ScaleMode, ViewSettingsState } from "./types";
|
||||
import { CHART_SYNC_ID, formatMetric, formatStepTick, fromLog1p } from "./utils";
|
||||
|
||||
const lossConfig = {
|
||||
displayLoss: { label: "Loss", color: "#3b82f6" },
|
||||
displaySmoothed: { label: "Smoothed", color: "#f59e0b" },
|
||||
} satisfies ChartConfig;
|
||||
|
||||
interface LossChartPoint {
|
||||
step: number;
|
||||
loss: number;
|
||||
smoothed: number;
|
||||
displayLoss: number;
|
||||
displaySmoothed: number;
|
||||
}
|
||||
|
||||
export function TrainingLossChartCard({
|
||||
data,
|
||||
domain,
|
||||
visibleStepDomain,
|
||||
xAxisTicks,
|
||||
avgRaw,
|
||||
avgDisplay,
|
||||
smoothing,
|
||||
setSmoothing,
|
||||
showRaw,
|
||||
setShowRaw,
|
||||
showSmoothed,
|
||||
setShowSmoothed,
|
||||
showAvgLine,
|
||||
setShowAvgLine,
|
||||
viewSettings,
|
||||
scale,
|
||||
setScale,
|
||||
outlierMode,
|
||||
setOutlierMode,
|
||||
}: {
|
||||
data: LossChartPoint[];
|
||||
domain: [number, number];
|
||||
visibleStepDomain: [number, number];
|
||||
xAxisTicks: number[];
|
||||
avgRaw: number;
|
||||
avgDisplay: number;
|
||||
smoothing: number;
|
||||
setSmoothing: (value: number) => void;
|
||||
showRaw: boolean;
|
||||
setShowRaw: (value: boolean) => void;
|
||||
showSmoothed: boolean;
|
||||
setShowSmoothed: (value: boolean) => void;
|
||||
showAvgLine: boolean;
|
||||
setShowAvgLine: (value: boolean) => void;
|
||||
viewSettings: ViewSettingsState;
|
||||
scale: ScaleMode;
|
||||
setScale: (value: ScaleMode) => void;
|
||||
outlierMode: OutlierMode;
|
||||
setOutlierMode: (value: OutlierMode) => void;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<Card data-tour="studio-training-loss" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm pl-2">Training Loss</CardTitle>
|
||||
<CardAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<HugeiconsIcon icon={Settings02Icon} className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-xs">Chart Settings</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex flex-col gap-1.5 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">Smoothing</Label>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{smoothing.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[smoothing]}
|
||||
onValueChange={([v]) => setSmoothing(v)}
|
||||
min={0}
|
||||
max={0.99}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showRaw}
|
||||
onCheckedChange={(value) => setShowRaw(Boolean(value))}
|
||||
>
|
||||
Show raw loss
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showSmoothed}
|
||||
onCheckedChange={(value) => setShowSmoothed(Boolean(value))}
|
||||
>
|
||||
Show smoothed loss
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={showAvgLine}
|
||||
onCheckedChange={(value) => setShowAvgLine(Boolean(value))}
|
||||
>
|
||||
Show average line
|
||||
</DropdownMenuCheckboxItem>
|
||||
<SharedChartSettings
|
||||
view={viewSettings}
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
outlierMode={outlierMode}
|
||||
setOutlierMode={setOutlierMode}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={lossConfig} className="-ml-3 h-[220px] w-full">
|
||||
<LineChart
|
||||
data={data}
|
||||
syncId={CHART_SYNC_ID}
|
||||
syncMethod="value"
|
||||
accessibilityLayer={true}
|
||||
margin={{ left: 0, right: 8 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
type="number"
|
||||
domain={visibleStepDomain}
|
||||
ticks={xAxisTicks}
|
||||
allowDataOverflow={true}
|
||||
allowDecimals={false}
|
||||
minTickGap={28}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
fontSize={10}
|
||||
tickFormatter={(value) => formatStepTick(Number(value))}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={domain}
|
||||
allowDataOverflow={true}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={4}
|
||||
fontSize={10}
|
||||
width={52}
|
||||
tickFormatter={(value) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return "0";
|
||||
const shown = scale === "log" ? fromLog1p(num) : num;
|
||||
return formatMetric(shown);
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_value, payload) =>
|
||||
`Step ${payload?.[0]?.payload?.step ?? ""}`
|
||||
}
|
||||
formatter={(_value, name, item) => {
|
||||
if (name === "displaySmoothed") {
|
||||
return [formatMetric(Number(item?.payload?.smoothed)), "Smoothed"];
|
||||
}
|
||||
return [formatMetric(Number(item?.payload?.loss)), "Loss"];
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{showAvgLine && (
|
||||
<ReferenceLine
|
||||
y={avgDisplay}
|
||||
stroke="#3b82f6"
|
||||
strokeDasharray="4 4"
|
||||
strokeOpacity={0.5}
|
||||
label={{
|
||||
value: `avg ${formatMetric(avgRaw)}`,
|
||||
position: "insideTopRight",
|
||||
fontSize: 10,
|
||||
fill: "#3b82f6",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRaw && (
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="displayLoss"
|
||||
stroke="var(--color-displayLoss)"
|
||||
strokeWidth={1.2}
|
||||
strokeOpacity={showSmoothed ? 0.35 : 1}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)}
|
||||
{showSmoothed && (
|
||||
<Line
|
||||
type="monotoneX"
|
||||
dataKey="displaySmoothed"
|
||||
stroke="var(--color-displaySmoothed)"
|
||||
strokeWidth={2.2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
connectNulls={true}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)}
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
22
studio/frontend/src/features/studio/sections/charts/types.ts
Normal file
22
studio/frontend/src/features/studio/sections/charts/types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export type ScaleMode = "linear" | "log";
|
||||
export type OutlierMode = "none" | "p99" | "p95";
|
||||
|
||||
export type LossHistoryItem = { step: number; loss: number };
|
||||
export type SmoothedLossItem = LossHistoryItem & { smoothed: number };
|
||||
|
||||
export interface TrainingChartSeries {
|
||||
lossHistory: LossHistoryItem[];
|
||||
lrHistory: { step: number; lr: number }[];
|
||||
gradNormHistory: { step: number; gradNorm: number }[];
|
||||
evalLossHistory: { step: number; loss: number }[];
|
||||
}
|
||||
|
||||
export interface ViewSettingsState {
|
||||
effectiveWindowSize: number;
|
||||
minWindow: number;
|
||||
allStepsLength: number;
|
||||
effectivePanOffset: number;
|
||||
maxPanOffset: number;
|
||||
setWindowSize: (value: number) => void;
|
||||
setPanOffset: (value: number) => void;
|
||||
}
|
||||
123
studio/frontend/src/features/studio/sections/charts/utils.ts
Normal file
123
studio/frontend/src/features/studio/sections/charts/utils.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import type { LossHistoryItem, OutlierMode, SmoothedLossItem } from "./types";
|
||||
|
||||
export const CHART_SYNC_ID = "train-metrics-sync";
|
||||
export const MAX_RENDER_POINTS = 800;
|
||||
export const DEFAULT_VISIBLE_POINTS = 160;
|
||||
|
||||
export const placeholderEvalData = [
|
||||
{ step: 0, loss: 2.8 },
|
||||
{ step: 50, loss: 2.4 },
|
||||
{ step: 100, loss: 2.0 },
|
||||
{ step: 150, loss: 1.7 },
|
||||
{ step: 200, loss: 1.5 },
|
||||
];
|
||||
|
||||
export function toLog1p(value: number): number {
|
||||
const safe = Number.isFinite(value) ? Math.max(value, 0) : 0;
|
||||
return Math.log10(safe + 1);
|
||||
}
|
||||
|
||||
export function fromLog1p(value: number): number {
|
||||
return Math.max(0, 10 ** value - 1);
|
||||
}
|
||||
|
||||
export function formatMetric(value: number): string {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
if (value === 0) return "0";
|
||||
if (value >= 1000) return value.toFixed(0);
|
||||
if (value >= 1) return value.toFixed(2);
|
||||
return value.toExponential(2);
|
||||
}
|
||||
|
||||
export function formatStepTick(value: number): string {
|
||||
if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(1)}k`;
|
||||
}
|
||||
return String(Math.round(value));
|
||||
}
|
||||
|
||||
export function compressSeries<T>(data: T[], maxPoints: number): T[] {
|
||||
if (data.length <= maxPoints) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const stride = Math.ceil(data.length / maxPoints);
|
||||
return data.filter(
|
||||
(_item, index) => index % stride === 0 || index === data.length - 1,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildStepTicks(min: number, max: number, targetCount = 6): number[] {
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
return [0, 1];
|
||||
}
|
||||
if (max <= min) {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
const stepSize = Math.max(1, Math.ceil((max - min) / (targetCount - 1)));
|
||||
const ticks: number[] = [];
|
||||
let current = min;
|
||||
|
||||
while (current < max) {
|
||||
ticks.push(current);
|
||||
current += stepSize;
|
||||
}
|
||||
|
||||
ticks.push(max);
|
||||
return Array.from(new Set(ticks));
|
||||
}
|
||||
|
||||
export function buildYDomain(values: number[]): [number, number] {
|
||||
const finiteValues = values.filter((value) => Number.isFinite(value));
|
||||
if (finiteValues.length === 0) {
|
||||
return [0, 1];
|
||||
}
|
||||
|
||||
const min = Math.min(...finiteValues);
|
||||
const max = Math.max(...finiteValues);
|
||||
|
||||
if (min === max) {
|
||||
const base = Math.abs(min);
|
||||
const pad = base > 0 ? base * 0.08 : 0.1;
|
||||
return [min - pad, max + pad];
|
||||
}
|
||||
|
||||
const pad = (max - min) * 0.12;
|
||||
return [min - pad, max + pad];
|
||||
}
|
||||
|
||||
function getUpperPercentile(values: number[], mode: OutlierMode): number | null {
|
||||
if (mode === "none") return null;
|
||||
const finiteValues = values.filter((value) => Number.isFinite(value));
|
||||
if (finiteValues.length < 3) return null;
|
||||
|
||||
const sorted = [...finiteValues].sort((a, b) => a - b);
|
||||
const q = mode === "p99" ? 0.99 : 0.95;
|
||||
const index = Math.max(
|
||||
0,
|
||||
Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)),
|
||||
);
|
||||
return sorted[index] ?? null;
|
||||
}
|
||||
|
||||
export function applyOutlierCap(values: number[], mode: OutlierMode): number[] {
|
||||
const cap = getUpperPercentile(values, mode);
|
||||
if (cap == null) return values;
|
||||
return values.map((value) => Math.min(value, cap));
|
||||
}
|
||||
|
||||
export function ema(data: LossHistoryItem[], alpha: number): SmoothedLossItem[] {
|
||||
if (data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let s = data[0].loss;
|
||||
return data.map((d) => {
|
||||
s = alpha * d.loss + (1 - alpha) * s;
|
||||
return { ...d, smoothed: +s.toFixed(4) };
|
||||
});
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ import {
|
|||
ZapIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useEffect, useRef, useState, type ReactElement, type ReactNode } from "react";
|
||||
import { useState, type ReactElement, type ReactNode } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useGpuUtilization } from "@/hooks";
|
||||
import { formatDuration, formatNumber, phaseColors, phaseLabel } from "./progress-section-lib";
|
||||
|
|
@ -52,6 +52,9 @@ export function ProgressSection(): ReactElement {
|
|||
etaSeconds: state.etaSeconds,
|
||||
currentNumTokens: state.currentNumTokens,
|
||||
isTrainingRunning: state.isTrainingRunning,
|
||||
lossHistory: state.lossHistory,
|
||||
lrHistory: state.lrHistory,
|
||||
gradNormHistory: state.gradNormHistory,
|
||||
})),
|
||||
);
|
||||
|
||||
|
|
@ -75,8 +78,6 @@ export function ProgressSection(): ReactElement {
|
|||
const { stopTrainingRun } = useTrainingActions();
|
||||
const gpu = useGpuUtilization(runtime.isTrainingRunning);
|
||||
const [stopDialogOpen, setStopDialogOpen] = useState(false);
|
||||
const localStartAtRef = useRef<number | null>(null);
|
||||
const [, setLocalTick] = useState(0);
|
||||
|
||||
const pct =
|
||||
runtime.totalSteps > 0
|
||||
|
|
@ -89,31 +90,7 @@ export function ProgressSection(): ReactElement {
|
|||
)
|
||||
: Math.round(runtime.progressPercent);
|
||||
|
||||
useEffect(() => {
|
||||
if (runtime.elapsedSeconds != null && runtime.elapsedSeconds >= 0) {
|
||||
localStartAtRef.current = Date.now() - runtime.elapsedSeconds * 1000;
|
||||
return;
|
||||
}
|
||||
if (runtime.currentStep > 0 && localStartAtRef.current == null) {
|
||||
localStartAtRef.current = Date.now();
|
||||
}
|
||||
}, [runtime.currentStep, runtime.elapsedSeconds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtime.isTrainingRunning) {
|
||||
return;
|
||||
}
|
||||
const timer = window.setInterval(() => {
|
||||
setLocalTick((prev) => prev + 1);
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [runtime.isTrainingRunning]);
|
||||
|
||||
const elapsed =
|
||||
runtime.elapsedSeconds ??
|
||||
(localStartAtRef.current == null
|
||||
? null
|
||||
: Math.max(0, Math.floor((Date.now() - localStartAtRef.current) / 1000)));
|
||||
const elapsed = runtime.elapsedSeconds;
|
||||
const derivedEta =
|
||||
elapsed != null && pct > 0
|
||||
? Math.round((elapsed * (100 - pct)) / Math.max(pct, 1))
|
||||
|
|
@ -125,6 +102,19 @@ export function ProgressSection(): ReactElement {
|
|||
? runtime.currentStep / elapsed
|
||||
: null;
|
||||
|
||||
const stoppedLoss =
|
||||
!runtime.isTrainingRunning
|
||||
? lastNonZeroValue(runtime.lossHistory) ?? runtime.currentLoss
|
||||
: runtime.currentLoss;
|
||||
const stoppedLr =
|
||||
!runtime.isTrainingRunning
|
||||
? lastNonZeroValue(runtime.lrHistory) ?? runtime.currentLearningRate
|
||||
: runtime.currentLearningRate;
|
||||
const stoppedGradNorm =
|
||||
!runtime.isTrainingRunning
|
||||
? lastNonZeroValue(runtime.gradNormHistory) ?? runtime.currentGradNorm
|
||||
: runtime.currentGradNorm;
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
section: "Hyperparams",
|
||||
|
|
@ -269,19 +259,19 @@ export function ProgressSection(): ReactElement {
|
|||
<div>
|
||||
<p className="text-xs text-muted-foreground">Loss</p>
|
||||
<p className="text-3xl font-bold tabular-nums tracking-tight">
|
||||
{runtime.currentLoss.toFixed(4)}
|
||||
{stoppedLoss.toFixed(4)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">LR</p>
|
||||
<p className="text-lg font-semibold tabular-nums">
|
||||
{runtime.currentLearningRate.toExponential(2)}
|
||||
{stoppedLr.toExponential(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Grad Norm</p>
|
||||
<p className="text-lg font-semibold tabular-nums">
|
||||
{formatNumber(runtime.currentGradNorm, 3)}
|
||||
{formatNumber(stoppedGradNorm, 3)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -352,6 +342,16 @@ export function ProgressSection(): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
function lastNonZeroValue(points: { value: number }[]): number | null {
|
||||
for (let i = points.length - 1; i >= 0; i -= 1) {
|
||||
const value = points[i]?.value;
|
||||
if (Number.isFinite(value) && value !== 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function GpuStat({
|
||||
label,
|
||||
icon,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue