Nodus for Modal users
View MarkdownThe Nodus Python SDK keeps Modal’s names wherever the concept is the same: App, @app.function, .remote(),
.map(), .spawn(), @app.cls with enter and exit hooks, Image, Volume, Secret and Sandbox. Most
apps move by changing import modal to import nodus. This page lists what is the same, what is spelled
differently and what Nodus adds.
Sign in and run
Section titled “Sign in and run”pip install nodus-computenodus login # opens the console, stores a key for each org you picknodus run app.py # like `modal run`: an ephemeral App, deleted when the entrypoint returnsnodus deploy app.py # like `modal deploy`: a persistent Appnodus serve app.py # like `modal serve`: redeploys when a file changesIn CI, set NODUS_API_KEY instead of running nodus login.
"""Quickstart: one Function called three ways.
Run it with `nodus run examples/python/quickstart/app.py --n 10`."""
import nodus
app = nodus.App("quickstart")
@app.function(cpu=1, memory="1Gi", max_cost=1)def square(x: int) -> int: return x * x
@app.local_entrypoint()def main(n: int = 10) -> None: print("remote:", square.remote(7)) # one call; blocks for the result call = square.spawn(8) # start without waiting print("spawned:", call.get(timeout=600)) print("map:", list(square.map(range(n)))) # one call per input, results in input orderSide by side
Section titled “Side by side”| Modal | Nodus |
|---|---|
import modal |
import nodus |
app = modal.App("x") |
app = nodus.App("x") |
@app.function(gpu="H100", timeout=3600) |
@app.function(gpu="H100", timeout="1h") (numbers are also seconds) |
f.remote(x), f.map(xs), f.spawn(x), call.get() |
The same |
f.local(x) |
The same |
@app.local_entrypoint() |
The same |
modal.Function.from_name("app", "f") |
nodus.Function.from_name("app", "f") (Function.lookup also works) |
@app.cls() with @modal.enter(), @modal.method(), @modal.exit() |
@app.cls() with @nodus.enter(), @nodus.method(), @nodus.exit() |
modal.Image.debian_slim().pip_install("torch") |
nodus.Image.debian_slim().pip_install("torch") |
modal.Image.from_registry(...), .apt_install, .run_commands, .env, .add_local_dir |
The same |
modal.Volume.from_name("v", create_if_missing=True) |
The same; vol.commit() and vol.reload() behave as in Modal |
modal.Secret.from_name("hf"), Secret.from_dict({...}) |
The same, plus Secret.from_dotenv(".env") |
modal.Sandbox.create(app=app, image=...) |
nodus.Sandbox.create(image=...) (no App needed) |
sb.exec("python", "-c", "..."), p.stdout.read(), p.wait() |
The same |
sb.open(path, "w") |
The same |
sb.tunnels() |
Returns a list of Tunnel(port, url, public); sb.tunnels.open(8080) is not available yet |
sb.snapshot_filesystem() |
The same (Beta) |
@modal.experimental.clustered(size=2) |
@nodus.clustered(size=2) (Beta) |
min_containers, max_containers, scaledown_window |
The same, or min_workers and max_workers |
await f.remote.aio(x) |
The same: every blocking call has an .aio form |
GPU strings use Modal’s spellings: "H100", "H100:2", "A100-80GB", "A10G", "L40S". A family such as H100
matches any of its variants; "H100!" pins the exact variant. A list such as ["H100", "H200"] accepts either.
What Nodus adds
Section titled “What Nodus adds”Nodus places every call on the cheapest capacity that finishes it on time, and it stops work before money runs out. These arguments have no Modal equivalent:
@app.function( gpu="H100", max_cost=40, # a hard cap in USD across this Function's workers checkpoint="/nodus/state", # files here are saved and restored if capacity is reclaimed interruptible=True, # allow cheaper interruptible capacity; progress is kept through the checkpoint region=["us", "eu"], # region classes, not provider regions)def train(lr: float) -> dict: ...
print(train.estimate(3e-4)) # dry-run: expected cost, cold and warm start, the hold it needsf.estimate(...)returns the expected cost and start time of one call before you run it.- A cold start on a GPU with no warm worker prints its expected wait, so a long first call is not a surprise.
- Errors are typed:
nodus.errors.InsufficientCreditsstates the amount needed and how to add credit. nodus.Job,nodus.Workspaceandnodus.llmcover batch jobs, development machines and inference with the same credentials.
Differences to know
Section titled “Differences to know”- Parametrized classes (
modal.parameter()) are not supported. Configure the class in its@nodus.enter()hook instead. CallingMyClass(arg=...)raisesnodus.errors.Unsupported. - The Python minor version of the image must equal yours, as in Modal.
Image.debian_slim()defaults to your version; a mismatch is refused before anything runs. - Web endpoints (
@modal.web_endpoint,@modal.asgi_app) are not available. Usenodus.InferenceEndpointfor model serving. modal.Dictandmodal.Queuehave no equivalent. Pass data through return values, a Volume or your own database.- Clustered Functions (Beta) run each
.remote()or.spawn()as one gang;.map()over a clustered Function raisesnodus.errors.Unsupported. - Timeouts and durations accept Go-style strings (
"90s","6h") as well as seconds. Days are not a unit. - Money is always a decimal amount in USD (
max_cost=40or"40.00").