Python SDK

neural_studio lets any Python process create a run on the Experiments page and stream metrics, console output, system telemetry, model statistics, artifacts and media to it. Inside a Neural Studio runtime it needs no setup; anywhere else it needs an API key.

import neural_studio as ns

run = ns.init(name="resnet-baseline", config={"lr": 3e-4, "batch_size": 256}, tags=["baseline"])

for step, batch in enumerate(loader):
    loss = train_step(batch)
    run.log({"loss": loss, "lr": scheduler.get_last_lr()[0]}, step=step)

run.finish()  # also runs automatically at process exit

Installing

  • In a runtime: the SDK is preinstalled in every environment image, so import neural_studio works in a notebook cell and in the terminal. The image carries no optional extras. For GPU telemetry run pip install nvidia-ml-py; for images from arrays, JPEG or WebP encoding and Pillow inputs run pip install numpy Pillow. Do not run pip install "neural-studio[media]" inside a runtime: it would replace the preinstalled SDK with the release on PyPI.
  • Anywhere else: pip install neural-studio (Python 3.10 to 3.13; the only dependencies are requests and psutil). The extras are neural-studio[gpu] (NVML bindings for GPU telemetry) and neural-studio[media] (numpy and Pillow). The package also installs the ns command.

Authentication

Inside a runtime the kernel and the web terminal carry a session key, so ns.init authenticates on its own and the workspace is known. Outside, a full-access API key is resolved in this order, first hit wins:

  1. the NEURAL_STUDIO_API_KEY environment variable;
  2. the api_key= argument to ns.init;
  3. inside a runtime only, the session key;
  4. api_key = "ns_..." in the config file.

The config file is the first of these that exists: $NEURAL_STUDIO_CONFIG, $XDG_CONFIG_HOME/neural-studio/config.toml, ~/.config/neural-studio/config.toml, ~/.neural-studio/config.toml. It is TOML with two keys, api_key and api_url. The API base URL is api_url=, then NEURAL_STUDIO_API_URL, then the config file, then https://api.neuralstudio.co. The key travels in the X-API-Key header.

A session key can only create and feed runs in its own workspace; a full-access key can also read run data and open SSH. Neither can launch a runtime or touch files, environments, secrets or billing.

ns.init

ns.init(name, **options) creates the run and returns a Run handle. All options are keyword-only.

ArgumentDefaultWhat it does
namerequired1 to 200 characters.
configNoneA dict of hyperparameters; the server's copy seeds run.config.
tagsNoneA list of strings.
workspaceNoneThe workspace's UUID. Required outside a runtime, also in offline mode; inside a runtime it defaults to the session's workspace, and echo $NEURAL_STUDIO_WORKSPACE_ID in the terminal prints it. Names are not accepted.
descriptionNoneUp to 4,000 characters.
api_keyNoneSee the resolution order above.
api_urlNoneHighest priority for the API base URL.
enable_system_metricsTrueStarts the background sampler (below). When False, samples passed to run.log_system_metrics are not sent either.
system_metrics_interval15.0Seconds between samples; must be positive.
autocaptureTrueRecords the Python version, platform, installed packages, CUDA availability and version, and the git commit, branch and dirty flag when the working directory is a repository. There are no framework hooks: nothing patches PyTorch, TensorFlow or JAX.
rankNoneDistributed rank, else the RANK then LOCAL_RANK environment variables, else 0. Every rank logs to the same run.
mode"online""offline" never opens a socket; see Offline runs.
settingsNoneAn ns.Settings (below).
capture_consoleTrueTees Python-level sys.stdout and sys.stderr into the run's Logs tab. Output reaches the terminal first and capture never blocks; \r redraws (tqdm) collapse to about one line a second. Writes that bypass sys.stdout are not captured.

ns.init also installs an exception hook and an exit hook, so a run that is never finished by hand is still closed when the process ends (see finish). There is no project, group, resume or context-manager form, and no module-level log function: log through the handle.

The run handle

run.id, run.workspace_id, run.name, run.account_id and run.rank are plain attributes. The rest:

  • run.log(metrics, step=None) appends scalars and never blocks: points land in an in-memory ring, a sealer thread writes them to a local SQLite spill file, and a sender thread ships gzip batches with retries. Names are 1 to 200 characters; bool becomes 0 or 1; NaN and Inf are accepted and the charts mark divergence instead of dropping it. step is a non-negative int; when omitted, each metric counts its own steps from 0. Values may also be media (below). Logging after finish raises RuntimeError.
  • run.config is a live mapping. run.config.update({...}) and run.config["epochs"] = 10 merge at the top level (a nested dict replaces the stored one) and send a background update; one update is at most 256 KiB of JSON. Offline runs journal updates for ns sync.
  • run.summary is a read-only {metric: {"last", "min", "max", "step"}} maintained locally with the server's rules: last and step only move forward, NaN and Inf never enter min or max.
  • run.step is the last explicit step or the highest automatic one.
  • run.finished is True once finish ran.

Images, tables and histograms

A value in run.log may be one media object or a list of 1 to 256 of the same kind. A key keeps one kind of media for the life of the run, and media keys match ^[\w\-./: ]+$.

run.log({
    "samples": [ns.Image(img.cpu().numpy(), caption=label) for img, label in examples],
    "predictions": ns.Table(columns=["id", "label", "score"], data=rows),
    "fc_weights": ns.Histogram(model.fc.weight),
}, step=step)
  • ns.Image(data, caption=None, *, format=None, thumbnail=True) takes PNG, JPEG or WebP bytes, a path to such a file, a numpy array (HxW, or HxWxC with 1 to 4 channels; floats in [0, 1] are scaled, booleans become 0 or 255) or a Pillow image. A torch tensor is not accepted: pass tensor.cpu().numpy(). Arrays are encoded as PNG with the standard library; format="jpeg" or "webp" and Pillow inputs need Pillow. Arrays larger than 4,096 px a side or 16.8 megapixels are downscaled; encoded bytes over those limits or over 4 MiB are refused. Captions are at most 500 characters.
  • ns.Table(columns=None, data=None, *, dataframe=None, caption=None) takes columns plus rows, or a pandas dataframe=. Cells are text (at most 10,000 characters), numbers, booleans or None; a non-finite number becomes None. At most 10,000 rows, 200 columns and 8 MiB as JSON.
  • ns.Histogram(values=None, *, bins=64, counts=None, edges=None, caption=None) cuts raw values (a list, a numpy array or a torch tensor; non-finite values are dropped) into bins equal bins, or takes explicit counts and one more strictly increasing edges. 1 to 512 bins.

Media is encoded on a background thread and sent in batches of up to 256 items or 32 MiB. The per-run and per-workspace caps are on the limits page.

Artifacts and attention maps

  • run.log_artifact(name, data, *, kind="file", content_type=None, step=None, metadata=None) attaches a file and never blocks. data is bytes, a path (opened on the upload thread) or a readable binary file object. Names match ^[\w\-. /]{1,200}$ with no ..; one artifact is at most 64 MiB and metadata at most 8 KiB. The server keys artifacts by (kind, name, step) and the last write wins; step is not defaulted, so an artifact logged without one replaces the previous artifact of that name. Offline runs do not upload artifacts and drop them after one warning.
  • run.log_attention(name, tensor, *, step=None, tokens=None, layer=None, dtype="float32") stores an attention map shaped [T,T], [H,T,T] or [B,H,T,T] (numpy or torch; only batch element 0 is kept) as an attention artifact the Artifacts tab renders as a heatmap. A map over 64 MiB is downcast to float16, and skipped with a warning if it still does not fit.

Model watching

run.watch(model, *, log="gradients", log_freq=100, stats=None, max_params=1024, scaler=None) registers hooks on a torch.nn.Module (PyTorch must be importable; the SDK never installs it). Every log_freq backward passes it records gradients/<param>/norm, parameters/<param>/<stat> and gradients/global_norm at the run's current step, right before the next run.log call's own points. log is "gradients", "parameters" or "all"; stats picks parameter statistics from norm, mean, std, absmax and zero_frac (default norm, mean, std) and is ignored for gradient-only watching; at most max_params parameters are watched, in model order. The returned watcher exposes manifest, watched_names, flush_pending() and remove(). A second call replaces the first. run.unwatch() flushes and detaches the hooks.

System metrics

A background thread samples the machine's CPU percentage and RAM in use through psutil, and per-GPU utilisation, memory, temperature and power through NVML when pynvml is importable. One sample every system_metrics_interval seconds, posted in batches of four. The System tab of the run shows them. run.log_system_metrics(sample) injects a sample by hand (keys cpu_percent, ram_used_bytes, gpu_utilization, gpu_memory_bytes, gpu_temperature_c, gpu_power_w); it needs the sampler to be enabled and raises RuntimeError in offline mode.

Finishing a run

run.finish(exit_code=0, *, timeout=30.0, status=None) is idempotent. The status is completed for exit code 0 and failed otherwise, unless status names one of completed, failed, stopped or disconnected. Within timeout seconds it detaches run.watch, stops console capture and the sampler, drains metrics (60% of the time left), console lines, media and uploads, logs one warning naming anything left behind, then marks the run finished on the server. Whatever could not be delivered stays in the spill file, with the finish owed, for ns sync.

A process that ends without calling finish is finished by the exit hook within 5 seconds: failed with exit code 1 after an uncaught exception, disconnected otherwise. A run whose process vanishes without either is marked disconnected by the server once its heartbeats stop.

Offline runs and ns sync

Every run seals its batches into ./.neural-studio/run-<id>.db (one more file per extra rank, run-<id>-r<rank>.db) before sending them, so a crash or a dead network loses nothing. ns.init(..., mode="offline") goes one step further and never opens a socket: no key is needed, the run gets a local id, and config updates and console lines are journaled too. Artifacts are the exception; they are dropped offline.

ns sync            # scans ./.neural-studio and replays anything undelivered
ns sync path/to/run-<id>.db

ns sync, or ns.sync(path=None) from Python, creates each offline run on the server (folding in the journaled config), replays metric batches, console lines and media, performs any owed finish (an offline run ends completed), and vacuums the file. Each batch carries a fixed id, so a replay is delivered exactly once and running ns sync again is a no-op. Credentials resolve exactly as for ns.init. The exit code is 0 when nothing is left pending and nothing was discarded, else 1; from Python the SyncReport has files_scanned, runs_created, batches_sent, batches_dropped, log_lines_sent, media_sent, media_dropped, runs_finished, unfinished, lost and clean.

In a runtime the working directory is ~/workspace, which is durable and counts against your storage pool. Keep the spill on the local disk instead:

run = ns.init(name="...", settings=ns.Settings(spill_dir="/tmp/ns-spill"))

Settings

ns.Settings is a frozen dataclass of pipeline knobs, all optional: ring_size (65,536 points in memory), seal_max_points (5,000), seal_interval_s (2.0), send_backoff_base_s (0.5), send_backoff_cap_s (60.0), spill_dir (./.neural-studio) and heartbeat_idle_s (30.0).

Workspace secrets

Inside a runtime, every secret the environment references by name is an environment variable. ns.secrets.get("HF_TOKEN") reads it and raises KeyError with a message that says why when it is absent (not defined in the workspace, not referenced by the environment, or not inside a runtime); ns.secrets.get(name, default) returns the default instead. ns.secrets.names() and ns.secrets.missing() list the names that were injected and the referenced names that did not resolve. Nothing here calls the API. See Isolation and secrets.

Errors

All SDK errors derive from ns.NeuralStudioError: NeuralStudioAuthError (no key could be resolved), NeuralStudioValidationError (a bad argument; also a ValueError), NeuralStudioConfigError (an unreadable config file) and NeuralStudioAPIError (a non-2xx response, with status_code, code, message and details; a network failure has status_code 0 and code network.error). A finished run raises plain RuntimeError on log, and an unknown finish status raises ValueError.

Environment variables

VariableMeaning
NEURAL_STUDIO_API_KEYAPI key; wins over every other source.
NEURAL_STUDIO_API_URLAPI base URL.
NEURAL_STUDIO_CONFIGPath of the config file to read first.
NEURAL_STUDIO_SESSION_API_KEYSet in a runtime: the session key, and the signal that the SDK is inside a runtime.
NEURAL_STUDIO_WORKSPACE_ID, NEURAL_STUDIO_SESSION_IDSet in a runtime: the default workspace and the session the run is attached to.
RANK, LOCAL_RANKDefault distributed rank.
NS_USER_SECRET_NAMES, NS_USER_SECRETS_MISSINGSet in a runtime: the manifests behind ns.secrets.