bladekit — Blade AI Fabric SDK + CLI
Run Python functions on the GPU fleet, from your laptop.
Install
Distribution bladekit, import bladekit, command blade (bladekit is installed too, as an alias of the same command). Python ≥ 3.10.
pip install bladekit
Also served, with the forge-shadow compatibility package, by the branded index
https://console.blade.sh/sdk/ (docs/INSTALL.md in the source repository).
import bladekit
app = bladekit.app("demo")
@app.function(gpu="A100", image="fabric/torch-2.x")
def embed(texts):
from sentence_transformers import SentenceTransformer
return SentenceTransformer("all-MiniLM-L6-v2").encode(texts).tolist()
@app.local_entrypoint()
def main():
print(embed.remote(["hello", "world"]))
$ blade setup # browser flow: mints + saves a token (defaults to https://cp.run.blade.sh)
$ blade setup --api-url https://cp.run.stg.blade.sh # ... or against another plane
$ echo "$FABRIC_TOKEN" | blade token set # headless alternative; or: --from-env FABRIC_TOKEN
$ blade run script.py # streams logs live, returns the result
$ blade logs <run_id> -f # follow logs with resume
$ blade status <run_id> # state + queue position
$ blade runs # list runs
$ blade cancel <run_id>
$ blade nodes # fleet: GPUs free/busy
$ blade --version # installed bladekit version
Object API
bladekit.app(name, image=...)(or the class,bladekit.App) — namespace of remote functions.@app.function(gpu=, cpu=, memory=, timeout=, image=, idle_timeout=)— register a function.imageis a catalogue alias (fabric/base) or a raw registry ref with an explicit tag/digest. Note: when the platform enforces a registry allowlist (FORGE_IMAGE_REGISTRY_ALLOWLIST), a raw ref (or an Image-DSLImage.from_registry(...)base) outside the allowed registries is rejected with a 400IMAGE_REGISTRY_FORBIDDEN— catalogue aliases are always allowed.@app.local_entrypoint()— the functionblade runinvokes locally.fn.remote(*args, **kwargs)— blocking; returns the result or re-raises the remote exception (with the remote traceback).fn.spawn(*args, **kwargs)— non-blocking; returns a handle with.get(timeout=)/.cancel().fn.local(...)— run locally without the fleet.
Successive .remote() calls of the same function in one process reuse the same
run (hot container, FIFO calls). At the end of blade run the SDK POSTs
/complete to drain the container immediately.
Container lifecycle hooks
An @app.cls() class is the stateful counterpart of @app.function: it is
instantiated once per container and its methods (marked with @app.method())
reuse that instance across calls. Three decorators run code at defined moments in
the container's life:
@bladekit.build()— runs once at image-build time to bake expensive, deterministic setup into the image layer (download weights, warm a compile cache), so every container starts with it already on disk. Until the control-plane image-build path executes it, the runner falls back to running it once per container, before the enter hooks — semantically weaker (per-container, not baked in) but correct.@bladekit.enter()— runs once per container before the first call (the usual place to load a model onto the GPU). Re-runs on a fresh instance after ablade servehot reload.@bladekit.exit()— runs once per container at graceful teardown, after the last call drains and before the process exits (flush a cache,volume.commit(), close a handle, emit a final metric). Exit hooks are best-effort: an exception in one is logged and the remaining hooks still run, so a failing teardown never wedges shutdown.
Within each kind, hooks run in definition order; across kinds the order is always build → enter → (calls) → exit.
@app.cls(gpu="RTX-A4500", image="fabric/torch")
class Embedder:
MODEL = "BAAI/bge-small-en-v1.5"
@bladekit.build()
def fetch_weights(self): # once at build time (fallback: per container)
from huggingface_hub import snapshot_download
snapshot_download(self.MODEL)
@bladekit.enter()
def load(self): # once per container
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(self.MODEL, device="cuda")
@app.method()
def embed(self, texts):
return self.model.encode(texts).tolist()
@bladekit.exit()
def flush(self): # once at teardown, best-effort
print("shutting down")
Embedder().embed.remote(["hello", "world"])
Parametrized classes
bladekit.parameter() turns one @app.cls definition into many independent
instances, one per distinct set of parameter values — the equivalent of
modal.parameter. Declare parameters as typed class attributes; a parametrized
class defines no __init__ (the values are set for you). Each distinct value set
gets its own run and its own container pool, and the bound values travel with
every call so AI Fabric routes it to the matching container.
@app.cls(gpu="RTX-A4500", image="fabric/torch")
class LLM:
model_name: str = bladekit.parameter(default="Qwen/Qwen2.5-1.5B-Instruct")
temperature: float = bladekit.parameter(default=0.7)
@bladekit.enter()
def load(self):
# each instance reads its own bound parameters
self.pipe = load_pipeline(self.model_name)
@app.method()
def generate(self, prompt):
return self.pipe(prompt, temperature=self.temperature)
# one definition, two parametrized instances → two container pools
LLM(model_name="Qwen/Qwen2.5-1.5B-Instruct").generate.remote("hi")
LLM(model_name="meta-llama/Llama-3.1-8B-Instruct").generate.remote("hi")
- The value type comes from the annotation (or an explicit
type=) and must be one ofstr,int,float,bool,bytes. A parameter without adefaultis required at call time. - Instantiate with keyword arguments only; unknown or wrongly-typed values
are rejected with
bladekit.InvalidArgumentError.
Fan-out: map, spawn_map
-
fn.map(iterable, ...)— blocking fan-out off(x)over the GPU fleet; yields results in input order (order="index", default) or arrival order. -
fn.starmap(iterable, ...)— likemap, but each element is a tuple splatted as positional args:(a, b)→f(a, b). -
fn.spawn_map(iterable, ...)—spawn+map: fire the whole fan-out and return aMapHandleimmediately, without blocking on results. The batch is created, all inputs are pushed, and it is finalized server-side; you then poll it at your leisure.fn.spawn_starmap(iterable, ...)is the tuple-splatting variant.handle = classify.spawn_map(urls) # returns at once — nothing awaited ... # do other work; the fleet churns for label in handle.results(): # collect later (order/return_exceptions here) ... print(handle.status()) # inspect progress handle.cancel() # stop the batch
The batch is durable server-side, so a handle survives a client disconnect — re-attach from another process with
blade map resume <map_id>.map()blocks and streams;spawn_map()hands you the handle and gets out of the way.
Dynamic batching: @bladekit.batched
@bladekit.batched auto-collects individual calls into a batch and invokes the
decorated function once per batch, then fans the per-call results back to each
caller. The function is written to take lists and return a list; callers still
invoke one item at a time. Stack it under @app.function (Modal order):
@app.function(gpu="RTX-A4500", image="fabric/torch")
@bladekit.batched(max_batch_size=32, wait_ms=50)
def embed(prompts: list[str]) -> list[list[float]]:
# `prompts` is the whole batch; return one vector per prompt.
return model.encode(prompts).tolist()
vec = embed.remote("hello") # one call in, one vector out — batched under the hood
A batch is dispatched as soon as either trigger fires: it reaches
max_batch_size inputs, or wait_ms milliseconds elapse after the batch's
first input. This is distinct from @app.concurrent, which forwards N concurrent
requests to a long-lived server, each handled independently — @bladekit.batched
fuses calls into one invocation so you amortize a fixed per-call cost (a model
forward pass, a tokenizer round-trip) across the whole batch.
Contract: positional args are transposed by position and keyword args by name, so
every call in a batch must share the same call shape; the function must return a
list of exactly len(batch) results (else the whole batch fails with
BATCH_RESULT_LENGTH_MISMATCH). If the function raises, every call in the batch
receives that exception.
Cloud bucket mounts
bladekit.CloudBucketMount mounts an external object-store bucket you own
(S3 / GCS / R2 / MinIO / Scaleway — anything speaking the S3 API) as a
filesystem path inside a function or deployed replica. Unlike bladekit.Volume
(an AI Fabric-managed network volume with generations and commit()), a bucket
mount is a live view of your bucket — AI Fabric only mounts it, it never stores
the objects. The runner FUSE-mounts each bucket at container start and unmounts
it on teardown.
Pass it in the same volumes= map as a Volume, keyed by mount path:
import bladekit, pathlib
app = bladekit.App("bucket-demo")
creds = bladekit.Secret.from_name("aws-credentials") # holds the bucket keys
@app.function(
image="fabric/base",
volumes={
"/mnt/data": bladekit.CloudBucketMount(
"my-data-bucket",
secret=creds,
read_only=True, # mount without write access
key_prefix="raw/2024/", # mount only this sub-path
),
},
)
def count_parquet():
return sum(1 for _ in pathlib.Path("/mnt/data").glob("*.parquet"))
@app.local_entrypoint()
def main():
print(count_parquet.remote())
bladekit.CloudBucketMount(bucket_name, *, secret, read_only=False, key_prefix=None, bucket_endpoint_url=None, requester_pays=False):
secret— required. Credentials come only from abladekit.Secret, never inline. The wire spec carries the secret name, never a value; the control plane resolves it and injects its keys (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, plus optionalAWS_SESSION_TOKEN) as container env vars, and the runner reads those at mount time. A credential never appears in the run body, argv, or a log line.read_only=True— mounted without write access.key_prefix="raw/2024/"— mount only objects under this bucket-relative key prefix (must end with/).bucket_endpoint_url=— S3 endpoint override for non-AWS stores (R2, MinIO, GCS-S3, Scaleway); omit for AWS S3.requester_pays=True— for buckets with S3 Requester Pays.
Store the credentials first with blade secret create aws-credentials AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…. At most 8 bucket mounts per
function; mount paths share the volume namespace (absolute, unique, outside
reserved paths).
Errors
bladekit.APIError(and subclasses) carry the API envelopecode+ human message.bladekit.RunError(and subclassesNodeLostError,ContainerFailedError,VersionMismatchError, …) describe platform failures with actionable hints.- A remote user exception is re-raised as the original exception when possible,
otherwise wrapped in
bladekit.RemoteFunctionError— never confused with a platform error.
Config
blade setup (browser token flow) and blade token set both write
~/.blade/config.toml. FABRIC_API_URL / FABRIC_TOKEN environment variables
take precedence; FABRIC_CONFIG points the CLI at another file. blade token show
says where each value came from.
blade token set never accepts the token as a positional argument — that would
leak it into ps aux, shell history and process monitors. Pass it on stdin
(echo "$TOK" | blade token set) or name the variable (--from-env FABRIC_TOKEN).
Inside a container, read the platform's variables under their FABRIC_* names
(FABRIC_SERVER_PORT, FABRIC_VOLUMES, FABRIC_PREEMPT_GRACE_SECONDS,
FABRIC_MAX_SESSION_SECONDS, FABRIC_TURN_URL / FABRIC_TURNS_URL /
FABRIC_TURN_SECRET). FABRIC_* and FORGE_* are reserved: Image.env() rejects
both prefixes.
Compatibility and migration (from forge / forge-shadow)
The SDK used to be forge-shadow (import forge, command forge). Nothing an
existing user does breaks on upgrade; the old names are deprecated and keep
working for a transition period (see "Removal" below).
| Before | Now | The old one… |
|---|---|---|
pip install forge-shadow |
pip install bladekit |
installs bladekit + the shims below |
import forge |
import bladekit |
works, DeprecationWarning once |
forge.<x>, forge.<module> |
bladekit.<x>, bladekit.<module> |
same objects, same modules |
forge command |
blade command |
same subcommands and options, notice on stderr |
ForgeError |
FabricError |
the same class, DeprecationWarning on use |
FORGE_TOKEN, FORGE_API_URL, FORGE_CONFIG, FORGE_VERBOSE, … |
FABRIC_* |
read when the FABRIC_* one is unset |
~/.forge/config.toml |
~/.blade/config.toml |
copied to the new path on first use, left in place |
image forge/torch |
image fabric/torch |
still accepted, same image |
Details:
forge-shadowis now a thin compatibility distribution (source insdk/compat/forge-shadow/), published at exactly the same version asbladekit, which it requires (bladekit==<version>). It ships theforgeimport shim and theforgecommand.bladekitalone does not ship aforgepackage: two distributions owning oneforge/directory is what pip deletes during an upgrade, andforgeis another project's name on public PyPI.pip install -U forge-shadowtherefore upgrades an existing install in place; new installs should usebladekit.- The
forgeimport shim resolves everyforge.<module>to the already importedbladekit.<module>object, soisinstance/exceptchecks and pickles that nameforge.*modules (written by an older client) keep working. New pickles namebladekit.*, which needs bladekit on the other side — every image the control plane builds has it, because the image'sforge-shadow==<version>pulls it in. - Environment:
FABRIC_<NAME>wins;FORGE_<NAME>is the fallback. When both are set to different values the SDK usesFABRIC_<NAME>and warns once. The control plane still injectsFORGE_*into containers, so apps deployed with the old SDK keep running unchanged, and importingbladekitin a container also exposes the user-facing ones under theirFABRIC_*names. - Catalog aliases: the SDK sends
fabric/<name>to the control plane asforge/<name>(the only spelling the server knows for now);blade image listshowsfabric/<name>. - Unchanged on purpose (server side, renamed separately): HTTP headers
(
X-Forge-*,Forge-Signature), hosts, theforge-runner/forge-sandboxdprocesses and their[forge-runner]log prefix, thefg_token prefix, the__forge_*__attributes the runner reads, and.forgeignore.
Removal. The deprecated names (forge import and command, forge-shadow,
ForgeError, FORGE_* fallback, ~/.forge migration, forge/ alias rewrite) are
kept for a transition period whose end is not decided yet; they are removed in a
future major release, announced in the changelog.
.forgeignore
Bundling tars the script's directory. Add a .forgeignore to exclude paths; it
uses exactly the .gitignore syntax and matching rules (* never crosses /,
a leading or middle / anchors at the bundle root, a trailing / matches
directories only, ** spans directories, ! re-includes). .git, .venv,
__pycache__ are always excluded.
Image.from_dockerfile(context_dir=...) filters its build context with a
.dockerignore at the context root instead, matched exactly like docker build
(patterns are relative to the context root, so *.py only matches top-level
files; use **/*.py for any depth). ! is not supported there yet.
Metadata
Release files for bladekit 4.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bladekit-4.7.0.tar.gz | 162.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bladekit-4.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 340.7 kB
Release files / bladekit-4.7.0.tar.gz
| Download URL | bladekit-4.7.0.tar.gz |
|---|---|
| Size | 162.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ad6c0cffbb77d50b9e0edd8366fe1ddaaed894e17150a7971359bbd9a19c6165
|
|
BLAKE2b-256 checksum How to use checksums |
dadf1d0c0ec14c1d55230b5e96d963d556ee105619f42d4d225695ec66757576
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.13.16
|
Release files / bladekit-4.7.0-py3-none-any.whl
| Download URL | bladekit-4.7.0-py3-none-any.whl |
|---|---|
| Size | 177.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
25605ab5c0fe3624c035e3e51b0ab88713d05f08f4a0ec9d2c9e757a530c1c34
|
|
BLAKE2b-256 checksum How to use checksums |
9c663138be27ccfb5a41ca4e66196bbb51e2967c37f24b163cb7e0b179f58f15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.13.16
|