From Weights & Biases

W&B records what happened. WeightsLab records what happened and lets you change it while the run is still going. The port is mostly mechanical, the part that needs thought is what to do with the freedom that opens up.

Migration notes

Your logging calls mostly disappear. In W&B you call wandb.log() at every point you want a number recorded. In WeightsLab you wrap the object that produces the number once, and it logs itself from then on:

# W&B, you log, every step, by hand
loss = criterion(outputs, targets)
wandb.log({"train/loss": loss.item()}, step=step)

# WeightsLab, you wrap the criterion once, at setup
criterion = wl.watch_or_edit(nn.CrossEntropyLoss(reduction="none"),
                             flag="loss", signal_name="train-loss-CE", log=True)
loss_per_sample = criterion(outputs, targets, batch_ids=ids)   # logs itself

Note reduction="none". That is not incidental, it is what makes the loss per sample rather than per batch, which is what lets the studio sort your data by loss, plot a per-sample trajectory in every grid cell, and show you which samples produced a spike. A batch mean cannot answer that.

Config becomes editable. wandb.config is frozen after init() by design, it describes the run. WeightsLab’s hyperparameters are live objects:

parameters = {"optimizer": {"lr": 0.001}, "batch_size": 16}
wl.watch_or_edit(parameters, flag="hyperparameters", poll_interval=1.0)

Change the learning rate in the studio and parameters changes underneath your loop. Read hyperparameters from the dict each step rather than caching them in locals at startup, or your loop will not see the edits.

There are no sweeps. W&B Sweeps launch many runs and compare them. WeightsLab is built around staying inside one run and steering it, edit the learning rate when the curve flattens, discard the samples that are poisoning it, keep going. If you need a sweep, keep using one; the two are not competing for the same job.

Runs are directories, not a cloud project. wandb.init(project=...) registers a run with a server. WeightsLab’s equivalent is an experiment directory, checkpoints, logs, and notebook.ipynb all live in it:

export WEIGHTSLAB_ROOT_LOG_DIR=~/experiments/exp1
weightslab start ~/experiments/exp1

Replaced parts

Weights & Biases

WeightsLab

wandb.init(project=..., config=...)

wl.serve(serving_grpc=True) plus an experiment directory (root_log_dir / $WEIGHTSLAB_ROOT_LOG_DIR)

wandb.config

wl.watch_or_edit(parameters, flag="hyperparameters"), editable live from the studio

wandb.log({"loss": x})

A watched loss or metric that logs itself; or save_signals() for anything computed by hand

wandb.log({"acc": a}, step=s) per-sample

wl.save_signals(preds_raw=..., targets=..., batch_ids=ids, signals={...}, preds=...)

wandb.watch(model)

wl.watch_or_edit(model, flag="model", device=device)

wandb.Table / wandb.Artifact for datasets

wl.watch_or_edit(dataset, flag="data", loader_name=...), the tracked dataframe is the table

run.summary

ai_report_generation(), or the report button, see Experiment Reports

wandb.finish()

wl.write_history(), wl.write_dataframe(), wl.keep_serving()

W&B Sweeps

No equivalent, edit hyperparameters live instead, or keep sweeping with your existing tool

System metrics panel

Automatic; resource/* signals, see Resource monitoring signals

Updated examples

Before, a typical W&B loop:

import wandb, torch.nn as nn

wandb.init(project="mnist", config={"lr": 0.001, "batch_size": 16})
config = wandb.config

model = CNN().to(device)
wandb.watch(model, log="all")

optimizer = optim.Adam(model.parameters(), lr=config.lr)
criterion = nn.CrossEntropyLoss()

for step in range(10_000):
    images, targets = next(train_iter)
    outputs = model(images)
    loss = criterion(outputs, targets)
    loss.backward(); optimizer.step(); optimizer.zero_grad()
    wandb.log({"train/loss": loss.item()}, step=step)

wandb.finish()

After, the same loop on WeightsLab:

import itertools
import weightslab as wl
import torch.nn as nn

parameters = {"optimizer": {"lr": 0.001}, "batch_size": 16}
wl.watch_or_edit(parameters, flag="hyperparameters", poll_interval=1.0)

model = wl.watch_or_edit(CNN().to(device), flag="model", device=device)

optimizer = wl.watch_or_edit(
    optim.Adam(model.parameters(), lr=parameters["optimizer"]["lr"]),
    flag="optimizer")

train_loader = wl.watch_or_edit(
    train_dataset, flag="data", loader_name="train_loader",
    batch_size=parameters["batch_size"], shuffle=True, is_training=True)

criterion = wl.watch_or_edit(
    nn.CrossEntropyLoss(reduction="none"),          # per sample, not per batch
    flag="loss", signal_name="train-loss-CE", log=True)

wl.serve(serving_grpc=True)

# Open-ended, so the studio's Pause button decides when to stop
# (see the Good Practice guide on open-ended training loops).
for step in itertools.count():
    images, targets, ids = next(train_iter)
    outputs = model(images)
    loss_per_sample = criterion(outputs, targets, batch_ids=ids)
    loss_per_sample.mean().backward(); optimizer.step(); optimizer.zero_grad()

wl.write_history(); wl.write_dataframe(); wl.keep_serving()

Three things to notice: the loop body lost its logging call, batch_ids=ids appeared (it is what ties a loss value to the sample that produced it), and the range became itertools.count().

Expanded UI documentation

In W&B you would…

In Weights Studio

Read charts on the run page

The plots board, plus merged comparison plots and an error band showing each step’s real batch extremes

Open a W&B Table to look at data

The data exploration board, and you can act on what you find, not only look

Filter a Table by a column

Quick filters, or ask the agent in plain language

Note a bad sample and fix it later

Tag or discard it now; it affects the next training step

Compare two runs side by side

Compare within a run, merge signals onto one chart, or load weights from an earlier step straight off a plot

Read the system metrics panel

Resource monitoring, on the same x axis as your loss curves