| name | lightning-studios |
|---|---|
| description | Manage Lightning AI Studios (cloud dev machines with CPUs/GPUs) - create, start, stop, delete studios, switch machine types, run commands in them (including long detached runs with live progress tracking), upload/download files, and SSH in. Use when the user wants to work with lightning.ai Studios, needs a cloud GPU dev box, or asks to run something "on a studio". Also load it before launching or resuming any detached run on a Studio, even one driven by your own helper scripts, a handoff note or the summary of a compacted conversation, and load it again after a compaction; every such run needs the live progress bar from lightning-jobs/progress.py. |
| license | Apache-2.0 |
| compatibility | Requires Python with uv or pip, the lightning CLI from the lightning-sdk package (installed on demand), network access to lightning.ai, and a Lightning AI account. |
A Studio is a persistent cloud development machine on lightning.ai. Its filesystem persists across sessions; compute (CPU/GPU) attaches on start and detaches on stop. Everything below uses the lightning CLI and lightning_sdk Python package.
export DEBUG=0 LIGHTNING_DEBUG=0 # an inherited DEBUG=1 logs the Authorization header; set this in every new shell
# Use the Lightning AI CLI from the current env; install or upgrade it there if it's missing or older than 2026.9.18
v=$(lightning --version 2>/dev/null | sed -n 's/^Lightning CLI version //p')
[ -n "$v" ] && [ "$(printf '%s\n' 2026.9.18 "$v" | sort -V | head -1)" = 2026.9.18 ] \
|| uv pip install -U lightning-sdk || python3 -m pip install -U lightning-sdk
lightning --version # must print "Lightning CLI version …"; if not, see the table below
lightning login # browser flow; or set env vars for headless use:
export LIGHTNING_API_KEY=... LIGHTNING_USER_ID=... # USER_ID optional: with it Basic auth, without it the key is a Bearer tokenThis uses, and if needed installs into, the agent's current environment (project venv, conda env,
…); then call plain lightning … everywhere. If setup doesn't go cleanly:
| Symptom | Fix |
|---|---|
No active env, externally-managed-environment, or a conflict with the project's own pins |
Install it on its own: uv tool install lightning-sdk (or pipx install lightning-sdk), and call "$(uv tool dir --bin)/lightning" if that isn't on PATH |
Still no Lightning CLI version line after installing |
Another lightning (PyTorch Lightning's) or an inactive env comes first: check command -v lightning, then activate the env or call its bin/lightning |
No such command, No such option or unexpected extra argument |
The CLI is too old: upgrade it where command -v lightning points (uv pip install -U lightning-sdk, or uv tool upgrade lightning-sdk) |
| Installing is blocked (read-only env or home, agent sandbox) | Run each command as UV_CACHE_DIR="${TMPDIR:-/tmp}/uv" UV_TOOL_DIR="${TMPDIR:-/tmp}/uv-tools" uvx lightning-sdk … |
SSL/CA errors on every call, even lightning --version, only inside an agent sandbox |
A **/*.pem read-deny rule hides certifi's cacert.pem: allow that one path; don't disable the sandbox |
NameResolutionError, only inside an agent sandbox |
If lightning api fails too, ask the user to allow lightning.ai and *.lightning.ai (/sandbox). If only studio/job commands or the SDK fail, they bypass the sandbox proxy: run just those outside it. Background pollers fail the same way, silently |
Credentials are stored in ~/.lightning/credentials.json.
The install above also makes lightning_sdk importable in that env, so Python snippets run with
its python. If the CLI came from a uv tool or uvx fallback instead, run one-off scripts with
uv run --with lightning-sdk python script.py. For code that stays in the user's project, ask,
then declare lightning-sdk as a dependency with the project's own tool (uv add, poetry add).
Every studio lives in a teamspace owned by either an organization or a user. Never guess. Resolve in this order:
- Explicit
--teamspace owner/teamspaceflag (owner = org or user name) /Teamspace("owner/teamspace")in Python (the separateorg=/user=arguments are deprecated). - Env vars
LIGHTNING_ORG,LIGHTNING_TEAMSPACE,LIGHTNING_USERNAME, or config defaults in~/.lightning/config.yaml(lightning config get teamspace). - Otherwise list the options and ask the user which org/teamspace to use:
lightning api /v1/memberships | jq -r '.memberships[] | [.ownerType, .name, .projectId] | @tsv'Persist the user's choice so they aren't asked again: lightning config set teamspace <owner>/<teamspace>.
From a scoped API key (an agent with no user to ask): --teamspace needs the owner's slug,
while /v1/memberships only gives the owner's id, and the same teamspace can appear twice (an
organization row and a user row with one projectId). Continue only if a single teamspace is
listed, prefer its organization row, and resolve the owner by ownerType (needs jq):
M=$(lightning api /v1/memberships)
if [ "$(printf "%s" "$M" | jq '[.memberships[].projectId] | unique | length')" != 1 ]; then
echo "several teamspaces: ask the user which one" >&2
else
ROW=$(printf "%s" "$M" | jq -c '(.memberships | map(select(.ownerType=="organization"))) + .memberships | .[0]')
TS=$(printf "%s" "$ROW" | jq -r .name); OID=$(printf "%s" "$ROW" | jq -r .ownerId)
if [ "$(printf "%s" "$ROW" | jq -r .ownerType)" = organization ]; then
OWNER=$(lightning api "/v1/orgs/$OID" | jq -r .name)
else # a personal teamspace; the search is fuzzy, so match the id exactly
OWNER=$(lightning api /v1/users/search -X GET -f "query=$OID" \
| jq -r --arg id "$OID" '.users[] | select(.id==$id) | .username')
fi
lightning config set teamspace "$OWNER/$TS" # or pass --teamspace "$OWNER/$TS" each time
fiSigned in as the wrong account. A saved teamspace can belong to a different login, and
lightning login reuses the current identity. To sign in separately, give the new login its own
credentials file and no inherited key, then check who you are and what you can see:
D=$(mktemp -d "${TMPDIR:-/tmp}/lightning-login.XXXXXX") && chmod 700 "$D" && echo "env file: $D/env"
printf '%s\n' 'unset LIGHTNING_API_KEY LIGHTNING_USER_ID LIGHTNING_AUTH_TOKEN' \
"export LIGHTNING_CREDENTIAL_PATH='$D/credentials.json' DEBUG=0 LIGHTNING_DEBUG=0" > "$D/env"
. "$D/env" && lightning login && lightning auth whoami \
&& lightning api /v1/memberships | jq -r '.memberships[] | [.ownerType, .name, .projectId] | @tsv'Shell variables don't carry over between agent commands, so start every later command with
. "$D/env" && (the path printed above) and pass --teamspace explicitly.
--teamspace always takes owner/teamspace. Omitting --name/--teamspace in an interactive terminal opens a picker menu; in scripts always pass them explicitly.
# create (registers the studio, does NOT attach compute)
lightning studio create --name my-studio --teamspace owner/teamspace [--cloud PROVIDER] [--studio-type TEMPLATE]
# start compute (blocking; --create makes it if missing)
lightning studio start --name my-studio --teamspace owner/teamspace --machine CPU [--create] [--interruptible]
lightning studio start --name my-studio --teamspace owner/teamspace --gpus L4:4 # --gpus and --machine are mutually exclusive
# manage
lightning studio list --teamspace owner/teamspace [--all] [--sort-by status] # default lists only your studios
lightning studio switch --name my-studio --teamspace owner/teamspace --machine A100 # requires Running studio
lightning studio stop --name my-studio --teamspace owner/teamspace
lightning studio delete my-studio --teamspace owner/teamspace -y # NAME is positional here; -y/--yes: required non-interactively
# ssh / one-shot connect (create + start + ssh)
lightning studio ssh --name my-studio --teamspace owner/teamspace
lightning studio connect my-studio --teamspace owner/teamspace --machine CPU
# machine names the CLI accepts (offline list; says nothing about availability)
lightning machine list [--json]Studio paths use lit://<owner>/<teamspace>/studios/<studio-name>/<path>. Exactly one side of a copy must be lit://; directories need -r.
lightning cp ./train.py lit://owner/teamspace/studios/my-studio/train.py # upload
lightning cp -r lit://owner/teamspace/studios/my-studio/logs/ ./logs # download dir
lightning ls lit://owner/teamspace/studios/my-studio # list files (-r recursive, --json)
lightning rm lit://owner/teamspace/studios/my-studio/old.txt [-r] [-f]cp -r copies a folder's contents, not the folder. lightning cp -r ./proj lit://…/studios/my-studio/
puts proj's files straight into the Studio's home, whether or not either path ends in /. To
keep the folder, name it in the destination: lightning cp -r ./proj lit://…/studios/my-studio/proj/.
Check the result with lightning ls -r before running anything that expects the files in place.
Uploads reach the machine late, even on a ready Studio. lightning cp and
studio.upload_file / upload_folder go through storage and return before the file is on the
Studio's disk, so a command launched right after can fail with No such file or directory. Wait
until the file is there before running anything that needs it: upload_landed in Example
workflows does this.
from lightning_sdk import Machine, Studio, Teamspace
ts = Teamspace("my-org/my-teamspace") # owner/teamspace; org=/user= are deprecated
studio = Studio(name="my-studio", teamspace=ts, create_ok=True) # create_ok=False -> error if missing
studio.start(machine=Machine.CPU) # blocking; interruptible=True for spot pricing
print(studio.status) # NotCreated|Pending|Running|Stopping|Stopped|Completed|Failed
# run commands — studio must be Running
out = studio.run("nvidia-smi") # raises RuntimeError on non-zero exit
out, code = studio.run_with_exit_code("pytest -q") # no raise on non-zero
out, code = studio.run_and_detach("python train.py", timeout=30) # keeps running in background
studio.upload_file("train.py", remote_path="train.py") # remote path relative to studio home
studio.download_file("outputs/model.ckpt", "model.ckpt")
studio.upload_folder("./src"); studio.download_folder("outputs/")
studio.switch_machine(Machine.A100) # change machine while running
studio.set_env({"WANDB_API_KEY": "..."}) # env vars; partial=True merges
studio.stop() # releases compute, keeps filesystem
studio.delete()Pass as Machine.<NAME> or string (Machine.from_str("A100") accepts name or slug):
- CPU:
CPU_SMALL,CPU(default, 4 cores),CPU_X_2/4/8/16; big-disk:DATA_PREP,DATA_PREP_MAX,DATA_PREP_ULTRA - GPU:
T4_SMALL,T4,T4_X_2/4/8,L4,L4_X_2/4/8,L40S,L40S_X_2/4/8,RTXP_6000(+_X_2/4/8),A100(+_X_2/4/8),H100(+_X_2/4/8),H200(+_X_2/4/8),B200,B200_X_8 A100_40GB*/A100_80GB*variants exist in the SDK but are hidden from CLI--machine(usable withstudio switchand in Python).
SKUs get added, so treat this list as a starting point, not a closed set. lightning machine list
shows the names your CLI accepts, not what a teamspace can launch.
Pick the machine from live data, per cloud account. A teamspace can launch on several cloud accounts, and the same GPU differs between them in name, price and wait. List what each account can start right now, fastest first:
from lightning_sdk import Teamspace
ts = Teamspace("my-org/my-teamspace")
FAMILY = "H100" # the GPU family the workload needs
OWN = {"lightning-baremetal", "lightning-public-prod"} # Lightning's own accounts sort first
# Accounts go by id: ts.cloud_accounts holds display names, and list_machines() returns [] for a name
rows = [(m.wait_time, m.cost, a.cluster_id, m) for a in ts.cloud_account_objs
for m in ts.list_machines(cloud_account=a.cluster_id) if m.family == FAMILY]
# then real waits before missing or placeholder ones (300 s, 10800 s), then fastest, then cheapest
key = lambda r: (r[2] not in OWN, r[0] in (None, 300, 10800), r[0] or 0, r[1] or 0)
for wait, cost, acct, m in sorted(rows, key=key)[:15]:
print(f"{acct:34} {m.name:28} x{m.accelerator_count} ${cost}/h wait ~{wait}s")cost is USD per hour and wait_time the expected seconds until a machine is free.
list_machines() with no argument merges several accounts without saying which row belongs to
which. Show the user the top rows with price and wait: the head of the sorted list, never the tail, and
check its first row against the request before anything else.
Prefer Lightning's own accounts (lightning-baremetal, lightning-public-prod). Dev and test
accounts can list GPUs they can't start: a broken region, a bare HTTP 500, no price available, or
a Studio that boots without its GPU. A wait_time of exactly 300 or 10800 s is a placeholder, not
an estimate. When an account fails to start, take the next row instead of retrying it.
Ask before starting any GPU, and wait for a yes. Show the machine, its account, the price per hour and the most the run can cost under its deadline. Ask even when the user named the GPU or gave a budget: a budget is a limit, not approval to spend it.
A Studio's cloud account is fixed at creation (studio switch can't move it), so create it on
the chosen row's account and start or switch it to that row's machine. What matters is that the
account sells that GPU at that count, not its name: the SDK maps H200, lit-h200-1 and
lit-h200-141gb-1 to one machine.
cloud= / --cloud takes the account id (cluster_id), not a display name.
Studio(..., cloud="Lightning Cloud") fails with 400 clusterID Lightning Cloud is invalid.
For quotes before login, GET /v1/core/accelerators?cloudProvider=<PROVIDER> needs no auth (the
lightning-cost-estimation skill has the provider values and the costing recipes). Don't invent a
catalog endpoint: /v1/accelerators, /v1/accelerator-catalog, /v1/pricing and
/v1/compute/accelerators all return code: 5.
Interruptible (spot) is a flag, not a machine type: --interruptible / interruptible=True.
Ask for a GPU the Studio's cloud doesn't sell and studio start comes up on CPU with only a
"hasn't been vetted" warning. The default cloud doesn't sell every GPU at every count (e.g. 1×
H200 isn't on AWS). The first workflow below guards against this:
- Create the Studio on an account that sells the GPU at your count (listing above). A reused
Studio keeps its old cloud: check it in
lightning studio list, and create a new one if wrong. - Do setup on CPU on that account, because installs, model downloads and CUDA source builds bill
at GPU rates otherwise. Builds need their target set, e.g.
TORCH_CUDA_ARCH_LIST=9.0for H100/H200. - After switching to that row's
Machine, wait until the Studio can run commands and check the hardware. No GPU means stop the Studio and go back to step 1; don't retry.
Every run you detach on a Studio gets a live progress bar, an ETA and setback tracking from
lightning-jobs/progress.py. This applies to every launch,
including one from a helper script you wrote yourself, not only the example workflow below.
Printing log lines from a wait loop, or asking the user to tail -f a file, doesn't count.
After the conversation is compacted, load this skill again before the next launch. The
summary keeps how to call progress.py, but not this rule, so later launches go unwatched.
progress.py ships with the lightning-jobs skill, which is installed next to this one (by the
plugin and by npx skills add). <LIGHTNING_JOBS_SKILL_DIR> is the base directory Claude Code gave
for lightning-jobs if it's loaded, or else <THIS_SKILL_DIR>/../lightning-jobs. Check that
<LIGHTNING_JOBS_SKILL_DIR>/progress.py exists and use that copy. Don't search the disk for
another progress.py: other checkouts can be older, and the last watcher started decides
which copy draws every bar. Then follow that skill's
Live progress, ETA and setbacks
section: the PROGRESS line format, the Monitor on events, and the status-line offer.
Only the launch and the watch command differ on a Studio.
Launch on the Studio (e.g. through studio.run_and_detach) from the script's folder,
unbuffered (-u) so PROGRESS lines reach the log while the run is going, and end the log with
PROGRESS_EXIT so a crash is told apart from success:
cd ~/src && nohup sh -c 'python -u train.py > train.log 2>&1; echo PROGRESS_EXIT $? >> train.log' </dev/null >/dev/null 2>&1Watch from your machine, as a background command outside the agent sandbox. --log is
relative to the Studio's home, so src/train.log is the file the launch writes:
python3 <LIGHTNING_JOBS_SKILL_DIR>/progress.py watch --studio exp-1 --log src/train.log --teamspace my-org/my-teamspaceIf progress.py isn't there, lightning-jobs isn't installed. Tell the user once that
installing it adds the live bar. Until then, every few minutes, report the last PROGRESS line
and any error from tail -n 40 of the log.
Prompts this skill handles: "spin up a GPU studio and run my training script", "copy this repo to my studio and start a long run", "SSH into exp-studio", "my studio is idle, stop it".
Set up on CPU, switch to a GPU, run, collect results, stop. Running comes before a Studio
can run commands, and studio.start() / lightning studio start can keep blocking after it can,
or never return. So start in the background and poll for readiness with a deadline (see Gotchas).
Other SDK calls (run*, switch_machine, stop) poll with no time limit of their own, so every
call below goes through bounded: a stalled call raises instead of billing forever, and the
except stops the Studio. The lightning-jobs skill reuses the helpers below, with their imports:
import hashlib, os, re, shlex, threading, time
from lightning_sdk import Machine, Studio, Teamspace
def bounded(fn, seconds):
"""fn()'s result, or TimeoutError after `seconds`: the SDK's own polls have no deadline."""
done, box = threading.Event(), {}
def _go():
try:
box["ok"] = fn()
except BaseException as e:
box["err"] = e
finally:
done.set()
threading.Thread(target=_go, daemon=True).start()
if not done.wait(seconds):
raise TimeoutError(f"{getattr(fn, '__name__', 'call')} still blocking after {seconds}s")
if "err" in box:
raise box["err"]
return box.get("ok")
def run_if_up(studio, cmd, seconds=120):
"""(output, exit code), or ("", -1) while the Studio isn't Running (starting, between machines)
or the command doesn't answer within `seconds`."""
try:
return bounded(lambda: studio.run_with_exit_code(cmd), seconds)
except (RuntimeError, TimeoutError):
return "", -1
def stop_quietly(studio):
"""Stop billing; fall back to `lightning studio stop` if this raises."""
if str(studio.status).endswith(("Running", "Pending")):
bounded(studio.stop, 300)
def wait_ready(studio, minutes=10, failed=()):
deadline = time.time() + minutes * 60
while time.time() < deadline and not failed:
out, code = run_if_up(studio, "python -c pass") # not `true`: see Gotchas
if code == 0 and "setting things up" not in out:
return
time.sleep(10)
raise failed[0] if failed else TimeoutError(f"{studio.name} never finished setup: use a new Studio")
def start_ready(studio, machine, minutes=10):
"""Start on `machine` in the background and return once it can run commands."""
if str(studio.status).endswith("Running") and studio.machine != machine:
raise RuntimeError(f"{studio.name} is already running on {studio.machine}: stop it or use a new name")
failed = [] # start() errors surface here, not in the thread
def _start():
try:
studio.start(machine=machine)
except Exception as e:
failed.append(e)
threading.Thread(target=_start, daemon=True).start()
wait_ready(studio, minutes, failed)
def upload_landed(studio, local, remote, minutes=3, skip=(".git", "__pycache__", ".venv", "node_modules")):
"""Upload a file or folder (paths relative to the Studio home), hidden files included, then
wait until the Studio's disk has every file with the right content: the upload returns before
it lands (see *Files*). Folders named in `skip` stay behind."""
files = [local]
if os.path.isdir(local):
files = []
for root, dirs, names in os.walk(local):
dirs[:] = [d for d in dirs if d not in skip]
files += [os.path.join(root, n) for n in names]
dest = {f: remote if f == local else f"{remote}/{os.path.relpath(f, local).replace(os.sep, '/')}"
for f in files}
for f in files:
bounded(lambda: studio.upload_file(f, dest[f], progress_bar=False), 600)
want = [hashlib.md5(open(f, "rb").read()).hexdigest() for f in files]
check = "cd ~ && md5sum " + " ".join(shlex.quote(dest[f]) for f in files)
deadline = time.time() + minutes * 60
while time.time() < deadline:
out, code = run_if_up(studio, check)
if code == 0 and re.findall(r"^[0-9a-f]{32}", out, re.M) == want:
return
time.sleep(5)
raise TimeoutError(f"{local} hasn't fully reached {studio.name} after {minutes} min")
ts = Teamspace("my-org/my-teamspace")
# the fastest, then cheapest, 1x H200 across the teamspace's cloud accounts (see *Machine types*)
rows = [(m.wait_time, m.cost, a.cluster_id, m) for a in ts.cloud_account_objs
for m in ts.list_machines(cloud_account=a.cluster_id)
if m.family == "H200" and m.accelerator_count == 1]
if not rows:
raise SystemExit("no account here sells a 1x H200: ask the user for another GPU or teamspace")
wait, cost, acct, gpu = min(rows, key=lambda r: (r[0] is None, r[0] or 0, r[1] or 0))
print(acct, gpu.name, f"${cost}/h", f"wait ~{wait}s") # confirm with the user before the GPU starts
studio = Studio("exp-1", teamspace=ts, cloud=acct, create_ok=True)
# an existing Studio of that name is reused as is, on whatever cloud it was created on
assert studio.cloud_account == acct, f"{studio.name} is on {studio.cloud_account}: pick a new name"
try: # any failure from here stops the Studio
start_ready(studio, Machine.CPU)
upload_landed(studio, "./src", "src") # returns once the files are on disk
out, code = run_if_up(studio, "cd ~/src && pip install -r requirements.txt", 30 * 60) # at CPU rates
assert code == 0, out or "pip install failed or ran over 30 min"
try: # the row picked from this account's list
bounded(lambda: studio.switch_machine(gpu), 15 * 60) # TimeoutError if it never lands
except Exception as e: # it can report this and switch anyway
if "cannot switch to a Studio" not in str(e):
raise
for _ in range(30): # so trust nvidia-smi, not the call
out, code = run_if_up(studio, "nvidia-smi --query-gpu=name --format=csv,noheader")
gpus = out.splitlines() if code == 0 else []
if gpus:
break
time.sleep(10)
print(gpus) # e.g. ['NVIDIA H200']
assert len(gpus) == 1 and "H200" in gpus[0], "wrong hardware: pick another account"
wait_ready(studio) # a new machine: wait until it can run commands
# the exit code goes to train.exit for this loop and to the log for progress.py;
# clear the last run's before launching
bounded(lambda: studio.run_and_detach("cd ~/src && rm -f train.exit && nohup sh -c 'python -u train.py > train.log 2>&1; c=$?; echo PROGRESS_EXIT $c >> train.log; echo $c > train.exit' </dev/null >/dev/null 2>&1", timeout=30), 120)
print(run_if_up(studio, "tail -n 40 ~/src/train.log")[0]) # within the first minute: crashes show up in seconds
# now start the watcher (*Live progress for detached runs*); this loop only guards the deadline
deadline = time.time() + 2 * 3600 # a bit over the expected run time
while run_if_up(studio, "test -f ~/src/train.exit")[1] != 0:
if time.time() > deadline:
raise TimeoutError(run_if_up(studio, "tail -n 40 ~/src/train.log")[0])
time.sleep(60)
assert run_if_up(studio, "cat ~/src/train.exit")[0].strip() == "0", \
run_if_up(studio, "tail -n 40 ~/src/train.log")[0]
except BaseException:
stop_quietly(studio)
raiselightning cp -r lit://my-org/my-teamspace/studios/exp-1/src/outputs/ ./outputs
lightning studio stop --name exp-1 --teamspace my-org/my-teamspaceOnce train.py is detached, start its watcher as in Live progress for detached runs:
progress.py watch --studio exp-1 --log src/train.log.
For a short run where setup is light (a uv script that declares its own dependencies), skip
the CPU phase but keep the try block and its deadline: start_ready(studio, gpu), then
upload_landed(studio, "train.py", "src/train.py"), drop the pip, switch and nvidia-smi steps,
and detach env -u UV_LIGHTNING_VIRTUALENV_ROOT uv run train.py in place of python train.py
(see Gotchas).
SSH in and run scripts interactively (for a human user; agents should prefer studio.run* above since ssh opens an interactive shell):
lightning studio ssh --name exp-1 --teamspace my-org/my-teamspace # interactive shell
lightning ssh configure --name exp-1 --teamspace my-org/my-teamspace # writes ~/.ssh/config Host block...
ssh exp-1 'python ~/src/eval.py' # ...then plain ssh runs one-off commands
lightning studio connect exp-1 --teamspace my-org/my-teamspace --machine CPU # create+start+ssh in one shotFor anything the CLI doesn't wrap, lightning api <path> makes an authenticated request (-X method, -F typed field, -f string field, -q jq filter):
lightning api /v1/memberships -q '.memberships[].name'
PROJECT_ID=$(lightning api /v1/memberships | jq -r '.memberships[] | select(.name=="<teamspace>") | .projectId' | head -1)
# studios are "cloudspaces" in the API — one word, and call it BARE (see Gotchas)
lightning api "/v1/projects/${PROJECT_ID}/cloudspaces" -q '.cloudspaces[].name'- Inherited
DEBUG=1leaks credentials. SDK HTTP diagnostics then print theAuthorizationheader, even on a config read, which is why the setup block setsDEBUG=0 LIGHTNING_DEBUG=0. Never publish such logs; if a key was printed, tell the user to rotate it (avatar → Global Settings → Keys). - A saved teamspace can belong to a different login. If resolving it fails, compare
lightning auth whoamiwithlightning api /v1/memberships, and never switch organizations on your own: a sole membership elsewhere doesn't replace the one requested. See Signed in as the wrong account. - Stop Studios when done: attached compute bills, and GPUs cost more.
start()on a Studio already running on a different machine raises. Useswitch_machineinstead.- Disabling auto-sleep (
studio.auto_sleep = False) or settingauto_sleep_timeconverts a free CPU studio to paid. - For a one-shot run (train, eval, batch), prefer a job (
lightning-jobs). A job stops billing when its command exits, crash included; a crashed run on a Studio keeps the GPU billing. The exception is a short run on a tight deadline: the first job from a Studio waits about 5 minutes for its snapshot, so run on a Studio started on the GPU, with a deadline that stops it. Pick one before showing the plan and name it there, with where the outputs will land. - Before relaunching on a GPU, check the last attempt isn't still holding it, or the new run
hits
CUDA out of memory:nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheadershould print nothing. - Read a detached run's log within the first minute, and treat a silent monitor as a failure.
A poller with no deadline, one that only matches progress lines, or one that can't reach
lightning.ai (a sandboxed background command) stays quiet through a crash. Give it a deadline,
match
Traceback/Error/Killed/CUDA out of memorytoo, and check it prints its first line. - A non-interactive
lightning studio deletewithout-ydeletes nothing. It printsAre you sure you want to delete? [y/N]: Aborted.and exits, leaving the studio (and its billing) alive. Confirm with the user first, then pass-y/--yes. - The studios list endpoint is
/cloudspaces, one word, and takes no-Ffields. The hyphenated/v1/projects/{pid}/cloud-spacesreturnsHTTP 404 Not Found. Adding-F limit=20fails withHTTP 400 Bad Request, becauselightning apisends any request with-f/-Ffields and no-Xas a POST (the create call). Call it bare and slice with-q, or pass-X GETto send the fields as query params. - Inside a Studio,
Studio()with no args resolves to the current studio (viaLIGHTNING_CLOUD_SPACE_ID). uv runfails in a fresh Studio (and in jobs launched from one) witherror: failed to symlink file from /system/conda/miniconda3/uv/cache/… to /system/conda/miniconda3/uv/venvs/…: No such file or directory. The Studio'sUV_LIGHTNING_VIRTUALENV_ROOTpoints at a folder that doesn't exist yet. Runenv -u UV_LIGHTNING_VIRTUALENV_ROOT uv run ….Runningis not ready: poll with apythoncommand until the setup message is gone. Right after a start, commands can fail withError: We are still setting things up for you, please try again after the progress bar at the top of the Studio disappears.python,pipanduvare shell aliases (load_conda_and_run) that refuse until setup is done, while a plaintruesucceeds much earlier, so pollstudio.run_with_exit_code("python -c pass")as in Example workflows. Setup usually takes 1–2 minutes. If it is still going after several minutes, stop the Studio and create a new one: starting the same one again can stay stuck.studio.switch_machine()can raiseApiException (400) … "cannot switch to a Studio"and still switch. Catch that error and pollnvidia-smifor the hardware, as in Example workflows; don't restart or re-switch on it.- Don't assume the folder a command starts in.
cdto the folder the upload landed in by its path from home (cd ~/src, as the examples do), andlsthe script there before the real run. - Detach with
nohup … </dev/null >/dev/null 2>&1. Without the redirects, every laterstudio.run*()output starts withnohup: ignoring input, which floods a log monitor. studio.run*()raises when the command's output isn't valid UTF-8. Output cut through a multi-byte character (tail -c Non a log withéor a progress bar,head -con a text file) fails incloud_space_service_get_long_running_command_in_cloud_space(HTTP 500), even though the command itself succeeded. Read logs withtail -n N, or pipe throughiconv -c -f utf-8 -t utf-8.- Passing both
teamspace="owner/teamspace"and the deprecatedorg=/user=raisesValueError(forTeamspace,StudioandJob). The old arguments alone still work but emit aDeprecationWarning.