关于 lightning-studios
lightning-studios 是一项技能,通过 `lightning` 命令行和 Python SDK 管理 Lightning AI 上的持久云开发机,涵盖 Studio 生命周期、算力选择和文件传输。
主要功能
- 01创建和管理 Studio
- 02启动或停止 CPU、GPU 算力
- 03使用 `lit://` 路径传输文件
- 04调用 `lightning_sdk` Python API
适用场景
- 01在持久云环境中开展开发工作
- 02为 Studio 工作负载选择算力
- 03上传项目文件或取回 Studio 输出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lightning-ai/ skills --skill lightning-studios安装后在 Claude Code 中输入/lightning-studios确认已加载。
详细介绍
Lightning AI Studios
Studio 是 lightning.ai 上一台持久化的云开发机器。其文件系统会在不同会话之间保留;计算资源(CPU/GPU)会在启动时挂载,在停止时卸载。以下所有操作都使用 lightning CLI 和 lightning_sdk Python 包。
设置与身份验证
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 token
这会使用 Agent 当前的环境(项目虚拟环境、conda 环境等),并在需要时安装到该环境中;之后到处都可以直接调用 lightning …。如果设置过程不顺利:
| 症状 | 解决方法 |
|---|---|
没有活动环境、出现 externally-managed-environment,或与项目自身的固定版本冲突 | 单独安装:uv tool install lightning-sdk(或 pipx install lightning-sdk),如果 lightning 不在 PATH 中,则调用 "$(uv tool dir --bin)/lightning" |
安装后仍没有 Lightning CLI version 行 | 另一个 lightning(PyTorch Lightning 的)或未激活的环境排在前面:检查 command -v lightning,然后激活环境或调用其 bin/lightning |
出现 No such command、No such option 或 unexpected extra argument | CLI 版本太旧:在 command -v lightning 指向的位置升级(uv pip install -U lightning-sdk 或 uv tool upgrade lightning-sdk) |
| 安装被阻止(环境或主目录为只读,Agent 沙箱) | 对每条命令运行 UV_CACHE_DIR="${TMPDIR:-/tmp}/uv" UV_TOOL_DIR="${TMPDIR:-/tmp}/uv-tools" uvx lightning-sdk … |
每次调用都出现 SSL/CA 错误(甚至运行 lightning --version 也如此),且仅发生在 Agent 沙箱中 | **/*.pem 读取拒绝规则隐藏了 certifi 的 cacert.pem:允许访问该路径;不要禁用沙箱 |
出现 NameResolutionError,且仅发生在 Agent 沙箱中 | 如果 lightning api 也失败,请让用户允许访问 lightning.ai 和 *.lightning.ai(/sandbox)。如果只有 studio/job 命令或 SDK 失败,说明它们绕过了沙箱代理:仅在沙箱外运行这些命令。后台轮询器也会以相同方式失败,且不显示错误 |
凭据存储在 ~/.lightning/credentials.json 中。
上述安装也会使 lightning_sdk 能在该环境中导入,因此 Python 代码片段会使用该环境的 python 运行。如果 CLI 来自 uv tool 或备用的 uvx,请使用 uv run --with lightning-sdk python script.py 运行一次性脚本。对于会留在用户项目中的代码,请先征求用户同意,然后使用项目自己的工具(uv add、poetry add)将 lightning-sdk 声明为依赖项。
解析组织和 teamspace(首先执行此步骤)
每个 Studio 都位于一个 teamspace 中,该 teamspace 归某个组织或用户所有。切勿猜测。 按以下顺序解析:
- 显式指定
--teamspace owner/teamspace标志(owner = 组织或用户名)/在 Python 中使用Teamspace("owner/teamspace")(单独的org=/user=参数已弃用)。 - 环境变量
LIGHTNING_ORG、LIGHTNING_TEAMSPACE、LIGHTNING_USERNAME,或~/.lightning/config.yaml中的配置默认值(lightning config get teamspace)。 - 否则,列出选项并询问用户要使用哪个组织/teamspace:
lightning api /v1/memberships | jq -r '.memberships[] | [.ownerType, .name, .projectId] | @tsv'
保存用户的选择,以免再次询问:lightning config set teamspace <owner>/<teamspace>。
使用受限作用域 API 密钥时(无法询问用户的 Agent):--teamspace 需要所有者的 slug,而 /v1/memberships 只会返回所有者的 id;同一个 teamspace 可能出现两次(一个 organization 行和一个 user 行,共用一个 projectId)。仅当列出的 teamspace 唯一时才继续,优先选择其 organization 行,并根据 ownerType 解析所有者(需要 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
fi
登录了错误的账号。 已保存的 teamspace 可能属于另一个登录账号,而且 lightning login 会沿用当前身份。若要单独登录,请为新登录指定独立的凭据文件,并且不要继承密钥,然后检查当前身份和可见内容:
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 变量不会在不同的 Agent 命令之间保留,因此后续每条命令都要以
. "$D/env" && 开头(路径见上方输出),并显式传入 --teamspace。
CLI 参考
--teamspace 始终采用 owner/teamspace 格式。在交互式终端中省略 --name/--teamspace 会打开选择菜单;在脚本中始终显式传入这两个参数。
# 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]
文件:lit:// 路径
Studio 路径采用 lit://<owner>/<teamspace>/studios/<studio-name>/<path> 格式。复制时必须恰好有一侧是 lit://;目录需要使用 -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 复制的是文件夹内容,而不是文件夹本身。 lightning cp -r ./proj lit://…/studios/my-studio/
会将 proj 中的文件直接放入 Studio 的主目录,无论任一路径是否以 / 结尾。若要保留文件夹,请在目标路径中指定该文件夹:lightning cp -r ./proj lit://…/studios/my-studio/proj/。
运行任何依赖文件已就位的操作之前,请使用 lightning ls -r 检查结果。
即使 Studio 已就绪,上传的文件也会延迟到达机器。 lightning cp 和
studio.upload_file / upload_folder 会通过存储传输,并在文件写入 Studio 磁盘之前就返回,因此紧接着启动的命令可能会因 No such file or directory 而失败。在运行任何需要该文件的操作之前,请等待文件到位:示例工作流中的 upload_landed 会完成此操作。
Python SDK
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()
机器类型
传入 Machine.<NAME> 或字符串(Machine.from_str("A100") 接受名称或 slug):
- CPU:
CPU_SMALL、CPU(默认,4 核)、CPU_X_2/4/8/16;大磁盘: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 - SDK 中提供
A100_40GB*/A100_80GB*变体,但它们不会显示在 CLI--machine中(可通过studio switch和 Python 使用)。
SKU 会不断新增,因此请将此列表视为起点,而非完整清单。lightning machine list
显示的是 CLI 接受的名称,而不是某个 teamspace 可启动的机器。
根据每个云账户的实时数据选择机器。 一个 teamspace 可以在多个云账户上启动,同一 GPU 在不同账户上的名称、价格和等待时间可能不同。列出每个账户当前可启动的机器,并按速度从快到慢排序:
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 是每小时的美元价格,wait_time 是机器空闲前预计等待的秒数。
不带参数调用 list_machines() 会合并多个账户的结果,但不会说明每一行属于哪个账户。向用户展示价格和等待时间靠前的条目:显示排序列表的开头,绝不要显示末尾,并且首先核对第一行是否符合请求。
优先使用 Lightning 自有账户(lightning-baremetal、lightning-public-prod)。 开发和测试账户可能会列出无法启动的 GPU:区域故障、裸 HTTP 500、no price available,或启动后没有 GPU 的 Studio。wait_time 恰好为 300 或 10800 秒时,这是占位值,不是估算值。如果某个账户无法启动,请改用下一行,而不是重试。
启动任何 GPU 前都要征求同意,并等待用户确认。 展示机器、所属账户、每小时价格,以及在截止时间内运行的最高费用。即使用户指定了 GPU 或给出了预算,也要征求同意:预算是限额,不代表同意花费。
Studio 的云账户在创建时就固定了(studio switch 无法更改),因此请在所选机器所属的账户上创建它,然后启动或切换到该行对应的机器。重要的是该账户提供相应数量的 GPU,而不是机器名称:SDK 会将 H200、lit-h200-1 和
lit-h200-141gb-1 映射到同一台机器。
cloud= / --cloud 接受账户 ID(cluster_id),而不是显示名称。
Studio(..., cloud="Lightning Cloud") 会报错:400 clusterID Lightning Cloud is invalid。
登录前查询报价时,GET /v1/core/accelerators?cloudProvider=<PROVIDER> 无需身份验证(lightning-cost-estimation 技能包含提供商取值和成本计算方法)。不要臆造目录端点:/v1/accelerators、/v1/accelerator-catalog、/v1/pricing 和
/v1/compute/accelerators 都会返回 code: 5。
可中断(spot)是一个标志,不是机器类型:--interruptible / interruptible=True。
在 GPU 上启动
如果请求的 GPU 不在 Studio 所属云账户提供的机器中,studio start 会以 CPU 启动,并且只显示“hasn't been vetted”警告。 默认云账户并非在所有数量配置下都提供每种 GPU(例如,AWS 上没有 1×
H200)。以下第一种工作流可避免此问题:
- 在提供所需数量 GPU 的账户上创建 Studio(见上方列表)。复用的 Studio 会保留旧云账户:在
lightning studio list中检查其账户;如果不正确,请创建新的 Studio。 - 在该账户上使用 CPU 完成设置,因为安装、下载模型和编译 CUDA 源码若使用 GPU,将按 GPU 费率计费。 编译时需要设置目标,例如 H100/H200 使用
TORCH_CUDA_ARCH_LIST=9.0。 - 切换到该行对应的
Machine后,等待 Studio 可以运行命令,并检查硬件。 如果没有 GPU,请停止 Studio 并返回第 1 步;不要重试。
分离运行的实时进度(必需)
在 Studio 上分离的每次运行,都会从 lightning-jobs/progress.py 获取实时进度条、预计剩余时间和挫折跟踪。 这适用于每次启动,包括通过你自己编写的辅助脚本启动,而不仅仅是下面的示例工作流。
在等待循环中打印日志行,或让用户对文件运行 tail -f,都不算。对话压缩后,请在下次启动前重新加载此技能。 摘要会保留如何调用 progress.py,但不会保留这条规则,因此后续启动将无人监控。
progress.py 随 lightning-jobs 技能一起提供,该技能与此技能安装在同一位置(通过插件和 npx skills add 安装)。如果已加载 lightning-jobs,<LIGHTNING_JOBS_SKILL_DIR> 是 Claude Code 提供的该技能基础目录;否则为 <THIS_SKILL_DIR>/../lightning-jobs。检查 <LIGHTNING_JOBS_SKILL_DIR>/progress.py 是否存在,并使用该副本。不要在磁盘上搜索其他 progress.py:其他检出版本可能较旧,而且最后启动的监控器决定了绘制所有进度条时使用哪个副本。然后遵循该技能的实时进度、预计剩余时间和挫折跟踪部分:PROGRESS 行格式、对 events 的监控,以及状态行提示。在 Studio 上,只有启动和 watch 命令有所不同。
在 Studio 上启动(例如通过 studio.run_and_detach),并从脚本所在文件夹启动;使用非缓冲模式(-u),以便运行期间 PROGRESS 行能写入日志,并在日志末尾输出 PROGRESS_EXIT,以便区分崩溃和成功:
cd ~/src && nohup sh -c 'python -u train.py > train.log 2>&1; echo PROGRESS_EXIT $? >> train.log' </dev/null >/dev/null 2>&1
在你的机器上监控,将其作为 Agent 沙箱外的后台命令运行。--log 路径相对于 Studio 的主目录,因此 src/train.log 就是启动命令写入的文件:
python3 <LIGHTNING_JOBS_SKILL_DIR>/progress.py watch --studio exp-1 --log src/train.log --teamspace my-org/my-teamspace
如果找不到 progress.py,说明未安装 lightning-jobs。告知用户一次,安装后即可显示实时进度条。在此之前,每隔几分钟报告最后一条 PROGRESS 行,以及对日志运行 tail -n 40 得到的任何错误。
示例工作流
此技能可处理的提示词:“启动一个 GPU Studio 并运行我的训练脚本”、“将这个仓库复制到我的 Studio 并开始长时间运行”、“通过 SSH 登录 exp-studio”、“我的 Studio 处于空闲状态,停止它”。
在 CPU 上设置,切换到 GPU,运行、收集结果并停止。 Studio 必须先进入 Running 状态才能运行命令,而 studio.start() / lightning studio start 可能在 Studio 已可运行后仍持续阻塞,也可能永远不返回。因此,请在后台启动,并设置截止时间轮询就绪状态(参见注意事项)。其他 SDK 调用(run*、switch_machine、stop)本身没有时间限制地轮询,因此以下每个调用都通过 bounded 执行:调用卡住时会抛出异常,而不会无限计费,except 则会停止 Studio。lightning-jobs 技能会复用下面的辅助函数及其导入:
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)
raise
lightning cp -r lit://my-org/my-teamspace/studios/exp-1/src/outputs/ ./outputs
lightning studio stop --name exp-1 --teamspace my-org/my-teamspace
分离 train.py 后,按分离运行的实时进度中的说明启动监控器:
progress.py watch --studio exp-1 --log src/train.log。
对于设置简单的短时运行(依赖项由脚本自身声明的 uv 脚本),可以跳过 CPU 阶段,但保留 try 块及其截止时间:start_ready(studio, gpu),接着运行 upload_landed(studio, "train.py", "src/train.py"),省略 pip、切换和 nvidia-smi 步骤,并使用 env -u UV_LIGHTNING_VIRTUALENV_ROOT uv run train.py 代替 python train.py 来分离运行(参见注意事项)。
通过 SSH 登录并交互式运行脚本(适用于人类用户;Agent 应优先使用上面的 studio.run*,因为 ssh 会打开交互式 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 shot
原始 API 备用方案
对于 CLI 未封装的操作,lightning api <path> 会发送经过身份验证的请求(-X 方法、-F 类型化字段、-f 字符串字段、-q jq 过滤器):
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'
注意事项
- 继承的
DEBUG=1会泄露凭据。 SDK 的 HTTP 诊断信息随后会打印Authorization请求头,即使只是读取配置也是如此,这就是为什么设置代码块会设置DEBUG=0 LIGHTNING_DEBUG=0。切勿公开此类日志;如果密钥已被打印,请告知用户轮换密钥(头像 → 全局设置 → 密钥)。 - 已保存的 teamspace 可能属于另一个登录账号。 如果无法解析,请对比
lightning auth whoami和lightning api /v1/memberships,切勿自行切换组织:在其他组织中只有一个成员资格,并不能取代用户指定的组织。请参阅登录了错误的账号。 - 使用完毕后请停止 Studios:附加的计算资源会产生费用,GPU 的费用更高。
- 在已于另一台机器上运行的 Studio 上调用
start()会引发异常。 请改用switch_machine。 - 禁用自动休眠(
studio.auto_sleep = False)或设置auto_sleep_time会使免费的 CPU studio 变为付费。 - 对于一次性运行(训练、评估、批处理),优先使用 job(
lightning-jobs)。 job 会在命令退出时停止计费,包括崩溃退出;在 Studio 上崩溃的运行则会继续产生 GPU 费用。例外情况是截止时间紧迫的短时运行:Studio 启动的第一个 job 会等待约 5 分钟来创建快照,因此应在以 GPU 启动的 Studio 上运行,并设置截止时间以便停止它。在展示计划前先做出选择,并在计划中说明选择了什么,以及输出会保存在哪里。 - 在 GPU 上重新启动前,请确认上一次尝试没有仍在占用它, 否则新运行会遇到
CUDA out of memory:nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader应不打印任何内容。 - 在分离运行启动后的第一分钟内读取日志,并将静默的监控视为失败。 没有截止时间的轮询器、只匹配进度行的轮询器,或无法访问 lightning.ai 的轮询器(例如在沙盒中运行的后台命令),都会在崩溃后一直保持静默。请设置截止时间,同时匹配
Traceback/Error/Killed/CUDA out of memory,并确认它能打印第一行。 - 非交互式运行
lightning studio delete时若不带-y,不会删除任何内容。 它会打印Are you sure you want to delete? [y/N]: Aborted.并退出,Studio(及其费用)仍会保留。请先向用户确认,然后传入-y/--yes。 - Studio 列表端点是
/cloudspaces,中间没有连字符,且不接受-F字段。 带连字符的/v1/projects/{pid}/cloud-spaces会返回HTTP 404 Not Found。添加-F limit=20会失败并返回HTTP 400 Bad Request,因为lightning api在请求带有-f/-F字段且没有-X时,会将其作为 POST(创建调用)发送。请直接调用并用-q截取结果,或者传入-X GET将字段作为查询参数发送。 - 在 Studio 内,不带参数的
Studio()会解析为当前 Studio(通过LIGHTNING_CLOUD_SPACE_ID)。 - 在全新的 Studio 中(以及从全新 Studio 启动的 job 中)运行
uv run会失败,并显示error: failed to symlink file from /system/conda/miniconda3/uv/cache/… to /system/conda/miniconda3/uv/venvs/…: No such file or directory。Studio 的UV_LIGHTNING_VIRTUALENV_ROOT指向一个尚不存在的文件夹。请运行env -u UV_LIGHTNING_VIRTUALENV_ROOT uv run …。 Running不代表已就绪:请轮询并运行python命令,直到设置提示消息消失。 启动后立即运行命令可能会失败,并显示Error: We are still setting things up for you, please try again after the progress bar at the top of the Studio disappears.python、pip和uv是 shell 别名(load_conda_and_run),设置完成前会拒绝运行;而普通的true会更早成功,因此请像示例工作流中那样轮询studio.run_with_exit_code("python -c pass")。设置通常需要 1–2 分钟。如果几分钟后仍未完成,请停止 Studio 并创建新的 Studio:再次启动同一个 Studio 可能仍会卡住。studio.switch_machine()可能引发ApiException (400) … "cannot switch to a Studio",但仍然会完成切换。 请捕获此错误并像示例工作流中那样轮询nvidia-smi以检查硬件;不要因此重启或再次切换。- 不要假设命令启动时所在的文件夹。 请根据上传路径从 home 切换到文件夹(如示例所示,运行
cd ~/src),并在实际运行前在那里用ls检查脚本。 - 使用
nohup … </dev/null >/dev/null 2>&1分离运行。 如果省略重定向,后续每次studio.run*()的输出都会以nohup: ignoring input开头,导致日志监控器收到大量无关内容。 - 当命令输出不是有效的 UTF-8 时,
studio.run*()会引发异常。 输出若截断了多字节字符(例如对包含é或进度条的日志运行tail -c N,或对文本文件运行head -c),就会在cloud_space_service_get_long_running_command_in_cloud_space(HTTP 500)中失败,即使命令本身已成功。请使用tail -n N读取日志,或通过管道传入iconv -c -f utf-8 -t utf-8。 - 同时传入
teamspace="owner/teamspace"和已弃用的org=/user=会引发ValueError(适用于Teamspace、Studio和Job)。单独使用旧参数仍然有效,但会发出DeprecationWarning。