Experiment Tracking
About
The d9d.tracker package sends the loss, the metrics and the hyperparameters of a training job to an experiment tracker. The trainer.logging.tracker section of the configuration selects the tracker by its provider field. Only the main process logs.
Choosing a Tracker
Log (log)
Writes each scalar as one line to the Python logger of d9d, next to the other messages of d9d. It needs no extra and no options. It does not log hyperparameters.
A log line looks like this:
W&B (wandb)
Logs to Weights & Biases. It needs the d9d[tracker-wandb] extra, a W&B account and the project in project. Log in with wandb login or set WANDB_API_KEY before you start the job.
The context becomes a prefix of the name, e.g. train/loss. The hyperparameters go to the config of the run. By default, W&B does not capture the terminal output: set capture_terminal_logs: true to capture it.
A resumed job continues the same run. W&B drops a value whose step is lower than the last step of the run. The job may have logged some steps after its last checkpoint before it stopped. After a resume, W&B keeps the old values of these steps and drops the new ones.
With mode: offline, W&B writes the run to local files, which wandb sync uploads later. W&B does not resume a run in offline mode.
TensorBoard (tensorboard)
Writes TensorBoard event files to the directory in log_dir. It needs the d9d[tracker-tensorboard] extra. It needs no account and no server.
To see the run, start TensorBoard on the parent directory. It shows each subdirectory as a run:
The context becomes a prefix of the name, e.g. train/loss. The name, the description and the hyperparameters of the run go to the Text tab, as the entry run.
A resumed job writes to the same directory. TensorBoard drops the values that the stopped job logged after its last checkpoint, so the run shows each step once.
MLflow (mlflow)
Logs to an MLflow tracking server or store. It needs the d9d[tracker-mlflow] extra and the name of the experiment in experiment. The tracker creates the experiment if it does not exist.
The context becomes a prefix of the name, e.g. train/loss. The nested hyperparameters become params with names such as run/lr. MLflow has no histogram type, so the tracker does not log histograms. The tracker sends the values in a background thread, so a step does not wait for the server.
A resumed job continues the same run. MLflow does not allow you to change a param, so the run keeps the hyperparameters of its first job. The job may have logged some steps after its last checkpoint before it stopped. After a resume, MLflow keeps both the old and the new values of these steps.
Aim (aim)
Logs to an Aim repository. It needs the d9d[tracker-aim] extra and the path or URL of the repository in repo. By default, it also records the CPU, GPU and memory usage and the terminal output.
To see the runs, start the Aim UI on the same repository:
Null (null)
Logs nothing.
What the Loop Logs
| Name | When | Value |
|---|---|---|
loss |
Every step | The loss of the step, averaged over its microbatches and ranks, weighted by loss_weight. |
l2_grad_norm_total |
Every step | The total gradient norm of the step, before clipping. |
| The names of the task metrics | Every logging.period_steps steps and on the last step |
The metrics that the task creates in create_metrics(), accumulated over the steps since the previous logging step. A nested name joins its keys with /. See Metrics Overview. |
Each value has the step number and the context stage=train.
Logging does not make the step wait for the GPU. A value waits in pinned host memory until the GPU has computed it, and reaches the tracker at a later logging call. The values can lag logging.max_pending_steps steps (2 by default) behind the current step. If they lag more, logging waits for the GPU to finish the oldest step. This also limits how far the host runs ahead of the GPU. Before a checkpoint and at the end of training, the loop writes all waiting values to the tracker. If training raises, the loop writes only the values that the GPU has already computed.
At the start of a run, the tracker also gets the hyperparameters: trainer.run.hparams from the configuration and the results of dump_hparams() of the task and of the model provider. Override dump_hparams() to log the settings of your own code, e.g. the size of the model.
Logging Your Own Values
The context of EVENT_TRAIN_READY holds the TrackerWriter of the run. Keep it in your event handler and log with log() and a value of one of the types in TrackerValue (see the API Reference). The tensors of a value can be on the GPU. Each value gets the step that is current at the call and the context stage=train. Only the main process writes to the tracker, so the tracker shows the values of rank 0.
This task logs a histogram of each weight matrix every 100 steps:
The histograms go to the writer and not to a metric. A histogram is a snapshot of the weights at one step, while a metric accumulates a value over logging.period_steps and synchronizes it across ranks. Only the main process writes, so with FSDP the tracker shows the shards of rank 0, and with pipeline parallelism the stage of rank 0.
A Histogram holds only the counts, so W&B, TensorBoard and Aim show the bin numbers on the x-axis, not the values. MLflow and the log tracker do not log histograms.
A call reads a tensor on the current CUDA stream. You can change the tensor in place after the call, on the same stream, without changing the logged value. The writer works only while the run is open, until train() returns. Later calls raise an error.
Adding a New Tracker
The configuration union and the tracker factory live in d9d/tracker/factory.py, so a new tracker is a change to d9d itself. Open a pull request with it. The examples below add Comet.
The Configuration
Create a Pydantic model for the tracker settings. It must contain a provider literal field. This field is the discriminator of the configuration union.
The Run
Implement BaseTrackerRun. Its write() gets a TrackerPoint with the name, the step and the context, and a value of one of the types in TrackerValue. The tensors of the value are already on the CPU. End the match with case _: that raises a ValueError, so that a value type that your tracker does not handle yet, e.g. after an update of d9d, fails loudly. A tracker that writes only some types on purpose, such as the log tracker, can skip the others with case _: pass.
If the tracker has no tags, build the name of the series with series_key() from d9d.tracker.provider.flat. It puts the context before the name, e.g. train/loss, as the W&B, TensorBoard and MLflow trackers do.
The Tracker
Implement BaseTracker. It opens runs and saves the state needed to resume them.
Registration
Add the configuration to the AnyTrackerConfig union in d9d/tracker/factory.py, and a case for it to tracker_from_config(). If the SDK is an optional dependency, add the extra tracker-<provider> to pyproject.toml. Import the tracker inside the case, so that d9d imports without the SDK and does not spend time to import an SDK that the job does not use:
API Reference
d9d.tracker
Unified interface for experiment tracking.
AnyTrackerConfig = Annotated[AimConfig | LogTrackerConfig | MLflowConfig | NullTrackerConfig | TensorBoardConfig | WandbConfig, Field(discriminator='provider')]
module-attribute
Union of all tracker configurations, discriminated by the provider field.
TrackerValue = Scalar | Histogram
module-attribute
A value that a tracker run can write.
BaseTracker
Bases: ABC, Stateful, Generic[TConfig]
Abstract base class for a tracker backend that opens runs.
A tracker is Stateful. A job restored from a checkpoint can then continue the same run, for example the
same Aim run hash.
from_config(config)
abstractmethod
classmethod
Creates a tracker from its configuration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TConfig
|
The backend-specific configuration. |
required |
Returns:
| Type | Description |
|---|---|
Self
|
The tracker. |
open(properties)
abstractmethod
Opens a run for the duration of the block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
properties
|
RunConfig
|
The configuration of the run. |
required |
Yields:
| Type | Description |
|---|---|
BaseTrackerRun
|
The active run to log metrics to. |
BaseTrackerRun
Bases: ABC
Abstract base class for an active tracking session (run) of a tracker backend.
A run writes values to the backend. It keeps no step or context: every call carries them in a TrackerPoint.
Training code does not call it directly, but logs through a TrackerWriter.
write(point, value)
abstractmethod
Writes a value.
A run raises a ValueError for a value type that it does not handle. A run that writes only some types on
purpose can skip the others instead. Calls come in the order the values were logged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
TrackerPoint
|
The series, step and tags of the value. |
required |
value
|
TrackerValue
|
The value. Its tensors are on the CPU, in the dtype they were logged with. |
required |
Histogram
dataclass
Histogram logged to a tracker.
Creating a Histogram raises a ValueError if counts is not one-dimensional.
Attributes:
| Name | Type | Description |
|---|---|---|
counts |
Tensor
|
The counts, one per bin. Shape: |
RunConfig
Scalar
dataclass
TrackerPoint
dataclass
tracker_from_config(config)
Creates the tracker that the provider field of the configuration selects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AnyTrackerConfig
|
The tracker configuration. |
required |
Returns:
| Type | Description |
|---|---|
BaseTracker
|
The tracker. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If the optional dependency of the provider is not installed. |
ValueError
|
If the configuration type is unknown. |
d9d.loop.component.TrackerWriter
Writer that logs values, including GPU tensors, to a tracker run without waiting for the GPU.
A call reads a GPU tensor on the current CUDA stream, so later in-place changes on that stream do not affect the logged value. The value reaches the tracker after the GPU has computed it, at a later call of the writer or when the run closes. Each value gets the step that is current at the call.
__init__(pending, schedule, context)
Constructs the TrackerWriter object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pending
|
PendingTrackerWrites
|
The queue that passes the writes to the tracker run. |
required |
schedule
|
JobSchedule
|
The job schedule whose current step is attached to a value at the call. |
required |
context
|
dict[str, str]
|
The tags attached to every value, e.g. |
required |
log(name, value, context=None)
Logs a value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the series. |
required |
value
|
TrackerValue
|
The value. Its tensors can be on any device. |
required |
context
|
dict[str, str] | None
|
Tags for this value only. They override the tags of the writer with the same name. |
None
|