Run a training job on rented GPUs from Python. Nodus places the work on the cheapest capacity that fits, checkpoints it, and resumes it if the machine is taken back — so a fine-tune that would have died at hour six finishes.
import nodus
with nodus.Client() as client:
wl = client.run(command=["python", "train.py"], budget=20)
print(client.wait(wl.id).status)pip install nodus_computeThe distribution is nodus_compute; the import is nodus.
nodus login --base-url https://your-api-addressIt prints a short code and opens your browser once. Approve the code there and
it writes your key to ~/.nodus/config.toml — nothing to copy, and no key on
your clipboard. --no-browser prints the address instead of opening it.
--base-url is the API address your account was given; export
NODUS_BASE_URL once and you can drop the flag. There is no built-in address:
a guessed one is either nobody's deployment or somebody else's.
Status: verified against a local test double; not yet run against a deployed control plane. The console endpoints it calls are being built.
nodus logoutThat deletes the stored key. The key itself keeps working until you revoke it in the console — deleting the local copy is not a revocation.
export NODUS_API_KEY=nk_live_…
export NODUS_BASE_URL=https://…Or pass them directly: nodus.Client(api_key=…, base_url=…).
Highest source wins, decided one setting at a time: explicit argument, then
environment, then ~/.nodus/config.toml. Environment above the file so a stale
login on the same machine can never outrank what CI injected.
With none of them the client raises ConfigurationError before it opens a
socket, naming what is missing — it does not dial a guess and hand you a
name-lookup error.
run() takes a flat brief and returns a Workload:
import nodus
with nodus.Client() as client:
wl = client.run(
image="python:3.11-slim",
command=["python", "train.py", "--epochs", "3"],
peak_memory_gb=24, # picks capacity that fits
expected_runtime_hours=6, # informs the estimate
budget=40, # a ceiling, not a reservation
)
done = client.wait(wl.id) # polls until the run is over
print(done.status, done.cost_now_usd)Nothing is strictly required. Omit image and the default python:3.11-slim
fills in; omit command and the image's own entrypoint runs. Everything else
narrows the search or bounds the cost; omit budget and the run is uncapped —
the SDK warns rather than inventing a ceiling on your money.
Your image must be able to fetch a small binary — it needs curl, wget,
or python3 on the PATH. Most ML images have one. A bare ubuntu image has
none of them, and a machine that cannot fetch the runner is a machine you are
billed for while it does nothing.
wait() polls until the workload is terminal and has no deadline of its
own. An 18-hour run is normal, and a client that gave up on one would not stop
it — the work would carry on and keep billing while your program believed it had
failed. A transient network failure does not end the wait either; only a
permanent one (a revoked key, an unknown workload) is raised.
Pass timeout_seconds= if you want a bound. It ends the waiting, not the run:
APITimeoutError is raised, the workload continues, and .cancel() is what
stops it.
for event in client.stream_events(wl.id):
print(event.type, event.payload)
print(client.logs(wl.id)) # the job's own stdout and stderr
print(wl.refresh().cost_now_usd) # charged plus what is accruing right nowlogs() is not a live tail. The log is a committed artifact, so it lags the
process by a checkpoint and raises NotFoundError until the first checkpoint
carries one — for live progress, watch the events.
AsyncClient mirrors Client method for method:
async with nodus.AsyncClient() as client:
wl = await client.run(command=["python", "train.py"])
done = await client.wait(wl.id)Every failure is a subclass of NodusError, so one except catches the lot.
The column that matters when writing a handler is whether the condition clears
on its own — the SDK already retries the ones that do, and never retries the
ones that do not.
| Raised | When | Clears on its own? |
|---|---|---|
ConfigurationError |
a setting is missing, before any request | never |
AuthenticationError |
the key is wrong or revoked | never |
SignatureError |
a signed request was rejected — the key is fine, the signature is not | never |
ValidationError |
the brief was rejected, with the reason | never |
IdempotencyConflictError |
an Idempotency-Key was reused with a different payload |
never |
NotFoundError |
no such workload | never |
BudgetExceededError |
the run would pass your account's spend cap | only if you lower the ask or raise the cap |
RateLimitError |
too many requests; honours Retry-After |
yes, with time |
CapacityUnavailableError |
nothing in the market fits the brief | yes |
APIConnectionError / APITimeoutError |
the network, not the API | yes |
APIError |
any other 4xx/5xx | for 5xx, usually |
Everything after a bare -- is your program's own command line, passed through
untouched:
nodus login --base-url https://your-api-address
nodus run --budget 20 -- python train.py
nodus get wl_…
nodus logs wl_…Apache-2.0. See LICENSE.