SAGEA LogoDocs & API
Cursus

Logging metrics & artifacts

Cursus logs everything through a module-level singleton — call cursus.log() from anywhere in your training code.

Recording metrics

cursus.log({"train/loss": 0.42}, step=epoch)
cursus.log({"train/loss": 0.42, "train/acc": 0.91}, step=epoch)
  • Keys are free-form dotted paths ("train/loss", "eval/acc")
  • Values must be numeric (ints, floats — anything float() accepts)
  • Always pass an explicit step — omitting it warns and records step 0, collapsing your curve onto one x-position

Background flushing

Points are queued and flushed in a background thread every 5 seconds or every 50 points, whichever comes first, as one bulk insert.

  • log() never blocks training
  • log() never raises — network failures retry 3 times, then warn and drop the batch
  • A dead server costs you points, never a crashed training job

Calling log() before init() (or after finish()) warns and drops.

Logging images

cursus.log_image("val/samples", "pred_epoch3.png", step=3)  # file path
cursus.log_image("val/samples", open("a.png", "rb").read(), step=3)  # bytes
 
from PIL import Image
cursus.log_image("val/samples", Image.open("a.png"), step=3)  # PIL
 
import numpy as np
cursus.log_image("val/samples", np.zeros((64, 64, 3), "uint8"), step=3)  # numpy
  • image is a file path, raw PNG/JPEG/WEBP bytes (sniffed by magic bytes), a PIL Image, or a numpy array
  • The last two need Pillow installed — it stays an optional, lazily-imported extra
  • Images upload direct to object storage over a short-lived presigned URL (plain requests PUT, no boto) — the server only signs
  • Caps: 5 MB per image, 500 images per run; re-logging a (key, step) overwrites the previous bytes
  • Same never-raises contract: bad inputs, oversize files, and network failures warn and drop

View images on the run's Charts tab: one image at a time with a step scrubber, per key. URLs expire after 15 minutes — reload the page for fresh ones.

Attaching artifacts

cursus.log_artifact("yolov8m", "runs/train/weights/best.pt",
                    type="model", description="map50: 0.61")
cursus.log_artifact("dataset", ["train.csv", "val.csv"], type="dataset")
  • Each call creates a new version of the named artifact in the run's project (artifacts belong to projects, not runs)
  • Accepts one path or a list; only file names (not directories) are stored; at most 1000 files per call
  • type is a free string ("model", "dataset", …); description is shown on the version
  • Uploads allow up to ~100 MB per file with a longer (120s) timeout
  • Call before finish() — versions record the producing run

Config sync

run = cursus.init(project="demo", config={"lr": 1e-4, "opt": {"beta": 0.9}})
 
cursus.config["seed"] = 7          # same object as run.config
cursus.config.update({"lr": 1e-5})  # debounced server sync
  • The init config is the frozen snapshot. run.config / cursus.config are the same mutable dict
  • update() syncs back debounced (one PATCH per 5s burst) and finish() flushes, so the final config always lands
  • Server merge is deep (nested dicts recurse, arrays/scalars replace), last-write-wins, capped at 100 top-level keys / 32 KB
  • Finished runs are immutable: updates after finish() stay local-only (the server answers 409, surfaced as a warning, never an exception)

Return values

CallSuccessSkip/failure
init()Run (.id, .name, .url, .config)raises
log()Nonewarns, drops
finish()Nonewarns (no active run: silent)
config.update()None (debounced sync)warns on sync failure
log_artifact()version payload dictNone + warning
log_image()media payload dictNone + warning

Design rule: init() and sweep control calls may raise; everything else degrades to warnings. Training jobs must survive Cursus outages.

What's next

On this page