Source code for nvalchemi.hooks.reporting.layouts.train

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Training Rich reporting layout."""

from __future__ import annotations

from collections.abc import Sequence

from rich.layout import Layout
from rich.panel import Panel
from rich.table import Table

from nvalchemi.hooks.reporting._scalars import ScalarSnapshot
from nvalchemi.hooks.reporting.layouts.base import (
    BaseRichLayout,
    RichMetricHistory,
    RichPreviewHistory,
)


[docs] class TrainingRichLayout(BaseRichLayout): """Rich dashboard layout for training workflows.""" def __init__(self) -> None: super().__init__( name="training", preferred_plot_keys=( "loss/total", "optimizer/lr", "scheduler/lr", "loss/energy/unweighted", "loss/forces/unweighted", ), latest_title="Latest", history_title="History", )
[docs] def render( self, snapshot: ScalarSnapshot | None, history: RichMetricHistory, *, title: str, precision: int, max_scalars: int | None, plot_keys: Sequence[str] | None, max_plots: int, plot_height: int, ) -> Layout: """Build a training-specific Rich dashboard. Parameters ---------- snapshot : ScalarSnapshot | None Latest scalar snapshot, or ``None`` before the first report. history : RichMetricHistory Retained scalar history keyed by metric name. title : str Dashboard title. precision : int Significant digits used for scalar values. max_scalars : int | None Maximum number of latest scalar rows. plot_keys : Sequence[str] | None Explicit plot key ordering override. max_plots : int Maximum number of plot panels. plot_height : int Plot height in terminal rows. Returns ------- Layout Renderable Rich layout with training metrics and progress. """ layout = Layout(name="root") layout.split_column( Layout(name="header", size=3), Layout(name="body"), Layout(name="messages", size=5), ) layout["body"].split_row( Layout(name="left", ratio=2), Layout(name="plots", ratio=3), ) layout["left"].split_column( Layout(name="latest", ratio=3), Layout(name="progress", size=9), ) layout["header"].update(self._build_header(snapshot, title)) layout["latest"].update( Panel( self._build_table(snapshot, precision, max_scalars), title="Latest Metrics", ) ) layout["progress"].update( Panel( self._build_progress(snapshot, precision), title="Progress", ) ) layout["plots"].update( Panel( self._build_plots( history, precision=precision, plot_keys=plot_keys, max_plots=max_plots, plot_height=plot_height, ), title="Training Curves", ) ) layout["messages"].update( Panel(self._build_messages(snapshot), title="Messages") ) return layout
[docs] def default_preview_history(self) -> RichPreviewHistory: """Return representative training metrics for preview rendering.""" return { "loss/total": (1.2, 0.86, 0.61, 0.43, 0.31, 0.24), "loss/energy/unweighted": (0.54, 0.39, 0.27, 0.19, 0.14, 0.11), "loss/forces/unweighted": (0.66, 0.47, 0.34, 0.24, 0.17, 0.13), "optimizer/lr": (1e-3, 1e-3, 8e-4, 5e-4, 2e-4, 1e-4), "scheduler/lr": (1e-3, 1e-3, 8e-4, 5e-4, 2e-4, 1e-4), }
def _build_progress( self, snapshot: ScalarSnapshot | None, precision: int, ) -> Table: table = Table.grid(expand=True) table.add_column("Field", overflow="fold") table.add_column("Value", justify="right", no_wrap=True) if snapshot is None: table.add_row("state", "waiting") return table table.add_row("rank", str(snapshot.global_rank)) if snapshot.event_count is not None: table.add_row("event", str(snapshot.event_count)) if snapshot.step_count is not None: table.add_row("step", str(snapshot.step_count)) if snapshot.batch_count is not None: table.add_row("batch", str(snapshot.batch_count)) if snapshot.epoch is not None: table.add_row("epoch", str(snapshot.epoch)) if snapshot.epoch_step_count is not None: table.add_row("epoch batch", str(snapshot.epoch_step_count)) self._add_scalar_row( table, snapshot, "training/progress_fraction", "progress", precision, suffix="%", scale=100.0, ) self._add_scalar_row( table, snapshot, "training/steps_per_s", "steps/s", precision, ) self._add_scalar_row( table, snapshot, "training/batches_per_s", "batches/s", precision, ) if "training/eta_s" in snapshot.scalars: table.add_row( "eta", self._format_duration(snapshot.scalars["training/eta_s"]) ) if "scheduler/lr" in snapshot.scalars: table.add_row( "scheduler lr", self._format_value(snapshot.scalars["scheduler/lr"], precision), ) elif "optimizer/lr" in snapshot.scalars: table.add_row( "optimizer lr", self._format_value(snapshot.scalars["optimizer/lr"], precision), ) return table