关于 lightning-jobs
lightning-jobs 指导编码 Agent 在 Lightning AI 云机器上运行命令,涵盖镜像、Studio 和多机器任务。内容还包括 CLI 与 Python SDK 的配置、任务监控及产物获取。
主要功能
- 01运行镜像和 Studio 任务
- 02配置多机器任务
- 03通过 CLI 和 Python SDK 管理任务
- 04查看状态、日志和产物
适用场景
- 01在 Lightning AI 上运行计算任务
- 02启动分布式多机器作业
- 03监控任务并取回输出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lightning-ai/ skills --skill lightning-jobs安装后在 Claude Code 中输入/lightning-jobs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
progress.pyjob_progress/__init__.pycore.pyevents.pysettings.pystatusline.pystore.pytracker.pywatch.pyreferences/progress.md详细介绍
Lightning AI Jobs
Job 会在专用云机器上运行命令,并在完成后终止。分为两种:镜像 Job(在任意 Docker 镜像中运行)和 Studio Job(在现有 Studio 环境的快照中运行)。多机器分布式 Job 是同一个 Job,只需设置 num_machines=N。旧版 MMT 类和 mmt CLI 命令组已弃用:每个 mmt 子命令都会引导你使用 lightning job <cmd>。
设置与身份验证
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 headless:
export LIGHTNING_API_KEY=... LIGHTNING_USER_ID=... # USER_ID optional: with it Basic auth, without it the key is a Bearer token
这会使用 Agent 当前的环境(项目 venv、conda 环境等),并在需要时安装到其中;之后在所有地方直接使用 lightning …。如果设置不顺利:
| 症状 | 解决方法 |
|---|---|
没有激活的环境、出现 externally-managed-environment,或与项目自身的固定版本冲突 | 单独安装:uv tool install lightning-sdk(或 pipx install lightning-sdk),如果它不在 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:允许访问该路径;不要禁用沙盒 |
仅在 Agent 沙盒中出现 NameResolutionError | 如果 lightning api 也失败,请让用户允许访问 lightning.ai 和 *.lightning.ai(/sandbox)。如果只有 studio/job 命令或 SDK 失败,它们绕过了沙盒代理:只需在沙盒外运行这些命令。后台轮询器也会以相同方式静默失败 |
上述安装也会让 lightning_sdk 可在该环境中导入,因此 Python 代码片段会使用该环境的 python 运行。如果 CLI 来自 uv tool 或 uvx 后备方案,则使用 uv run --with lightning-sdk python script.py 运行一次性脚本。对于保留在用户项目中的代码,先征求同意,然后使用项目自身的工具(uv add、poetry add)将 lightning-sdk 声明为依赖项。
首次运行:尚未安装 CLI 或创建账户
用户可能从未听说过 Lightning,也可能没有点名要求使用它。用一句话说明 Job 将在 Lightning AI 上运行及原因(例如“这里没有 GPU”),然后帮他们完成设置,而不是遇到第一个错误就停止。运行上方的设置代码块(如果 CLI 尚未安装,它会安装),然后检查是否有用户已登录:
lightning auth whoami || echo "not signed in"
如果没有人登录,请根据机器情况选择合适的方式:
- **有浏览器(笔记本电脑):**运行
lightning login。它会打开 lightning.ai,新用户可以在那里创建免费账户。浏览器登录完成后命令才会返回,因此请等待几分钟,并告知用户正在打开新标签页。 - 没有浏览器(Claude Code 云会话、CI、远程机器):请用户在 lightning.ai 注册,然后从其头像 → 全局设置 → 密钥 → 通过 CLI 登录中复制
LIGHTNING_USER_ID和LIGHTNING_API_KEY。在 Agent 运行环境中将它们设为环境变量。切勿在聊天中索要密钥。在 Claude Code 云会话中,这两个值都应添加到云环境的环境变量中。同一对话框中的网络访问必须设为自定义,并允许访问lightning.ai和*.lightning.ai,同时保留默认列表。两项更改都只会在新会话中生效。
然后继续执行确定组织和团队空间。新账户只有一个团队空间,因此无需询问。
解析组织和 teamspace(首先执行此步骤)
作业位于由组织或用户拥有的 teamspace 中。切勿猜测。 使用明确的 --teamspace owner/teamspace 标志(Python:teamspace="owner/teamspace"),或环境变量 LIGHTNING_ORG / LIGHTNING_TEAMSPACE,或配置默认值(lightning config get teamspace)。如果都未设置,请列出选项并询问用户要使用哪个组织/teamspace:
lightning api /v1/memberships | jq -r '.memberships[] | [.ownerType, .name, .projectId] | @tsv'
保存此选择:lightning config set teamspace <owner>/<teamspace>。
job run 上单独的 --org/--user 标志,以及 Python 的 org=/user= 参数已弃用,建议改用组合形式,该形式可通过环境变量身份验证在无头模式下运行。CLI 会针对这些标志打印弃用警告,并将在之后移除它们。它们目前仍可使用,因此无需改写现有命令。
使用范围受限的 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 参考
子命令:run、list、inspect、logs、ssh、stop、delete、rename、tag、untag。inspect、logs 和 ssh 可接受 --rank N,用于选择多机器作业中的一台机器;对于单机作业,使用 --rank 会报错。
# image job
lightning job run --name my-job --teamspace owner/teamspace \
--image python:3.11-slim --machine CPU \
--command "python -c 'print(\"hello\")'" \
[-e KEY=VALUE ...] [--interruptible] [--cloud PROVIDER]
# studio job (snapshot of an existing studio's environment; command required)
lightning job run --name my-job --teamspace owner/teamspace \
--studio my-studio --machine A100 --command "python train.py"
# omit both --studio and --image while running inside a Studio to target THAT Studio
# (only if its teamspace matches the resolved --teamspace) — no lookup needed:
lightning job run --name my-job --teamspace owner/teamspace --machine A100 --command "python train.py"
# private registry image
lightning job run ... --image-credentials <secret-name> [--cloud-account-auth] # --cloud-account-auth for ECR-type registries
# monitor / manage
lightning job list --teamspace owner/teamspace [--all] [--sort-by status] [--json] # includes multi-machine jobs
lightning job inspect my-job --teamspace owner/teamspace # JSON incl. status, machine, cost
lightning job stop my-job --teamspace owner/teamspace
lightning job delete my-job --teamspace owner/teamspace -y # -y/--yes: required non-interactively
# logs — snapshot by default; --follow streams a running job until it finishes or Ctrl-C
lightning job logs my-job --teamspace owner/teamspace [--follow] [--tail 100] [--timestamps]
lightning job logs my-job --teamspace owner/teamspace --query error --severity error # filter server-side
lightning job logs my-job --teamspace owner/teamspace --since 2h --until 30m # window: duration (30s/2h/3d/1w) or RFC3339
# ssh into a running job (fails unless status is Running)
lightning job ssh my-job --teamspace owner/teamspace
# multi-machine training: same command plus --num-machines
lightning job run --name my-mmt --teamspace owner/teamspace \
--image pytorch/pytorch:2.4.1-cuda12.1-cudnn9-runtime --num-machines 2 --machine L4 \
--command "python -m torch.distributed.run --nproc_per_node=1 train.py"
# multi-machine logs: every rank merged and labelled; --rank N reads one machine
lightning job logs my-mmt --teamspace owner/teamspace [--follow] [--tail 50] [--rank 1]
lightning job ssh my-mmt --teamspace owner/teamspace # rank 0 by default
lightning job ssh my-mmt --rank 1 --teamspace owner/teamspace # pick a worker
- 没有
status子命令:请从inspect中读取status。 它会打印纯 JSON(也接受--json),包含command、image、machine、name、status、studio、tags、teamspace和total_cost,因此lightning job inspect <name> | jq -r .status可用作轮询数据源。要一次查看多个作业,lightning job list --json会包含状态、started_at/stopped_at、total_cost和num_machines。 Pending作业可能还没有日志,因为尚未安排机器。作业开始运行后,logs会返回当前可用的日志。ssh面向人类用户。 它会打开交互式 Shell,因此 Agent 应优先使用inspect、logs和 Python SDK。对于 Pending、Completed、Failed 或 Stopped 状态的作业,它会报出清晰的错误。
Python SDK
from lightning_sdk import Job, Machine, Status, Studio, Teamspace
ts = Teamspace("my-org/my-teamspace") # owner/teamspace; org=/user= are deprecated
job = Job.run(
name="my-job", # on a clash the platform renames it and the SDK warns
machine=Machine.CPU, # or "A100", Machine.from_str("L4"), ...
image="python:3.11-slim", # OR studio=<Studio|name> — mutually exclusive
command="python train.py", # required for studio jobs, optional for image jobs
teamspace=ts,
env={"RUN_MODE": "prod"},
interruptible=False, # True = spot: cheaper, can be preempted
max_run_attempts=2, # optional retries; None = backend default
)
print(job.link) # web UI URL
job.wait(interval=10, timeout=3600, stop_on_timeout=True) # blocks until terminal
print(job.status) # Status.Pending / Running / Stopping / Stopped / Completed / Failed
if job.status == Status.Failed:
print(job.logs) # snapshot of logs so far (also: `lightning job logs <name>`)
print(job.total_cost) # USD
job.stop(); job.delete()
# distributed job — same Job.run plus num_machines; per-worker access via .machines
mmt = Job.run(name="my-mmt", num_machines=2, machine=Machine.L4, image="...", command="...", teamspace=ts)
mmt.wait()
for worker in mmt.machines: # rank-ordered; each worker is a Job
print(worker.name, worker.status) # per-node logs via worker.logs; mmt.logs (or `lightning job logs`) merges all ranks
获取现有作业:Job("my-job", teamspace=ts)(如果作业不存在,会引发 ValueError)。
将 studio= 和 image= 都留空,会将目标设为你当前所在的 Studio(通过 LIGHTNING_CLOUD_SPACE_ID),这与在 CLI 中同时省略 --studio 和 --image 相同。在 Studio 外部,或 Studio 的 teamspace 与目标不匹配时,两种形式都会报错,并要求明确指定其中一项。
镜像作业与 Studio 作业
| Studio 作业 | 镜像作业 | |
|---|---|---|
command | 必需 | 可选(回退到镜像 entrypoint) |
entrypoint、image_credentials、cloud_account_auth | 禁止 | 允许 |
| artifacts | 将输出写入作业的 home($LIGHTNING_ARTIFACTS_DIR);见下文 | 默认无;通过 path_mappings={"<container-path>": "<connection>:<path>"} 指定路由 |
| scratch disks | scratch_disks={"data": 100}(GiB,位于 /teamspace/scratch/ 下) | 禁止 |
输出与工件(Studio 作业)
Studio 作业的主目录位于当前 Studio 的主目录挂载点 /teamspace/studios/this_studio。无论从哪个 Studio 启动作业,路径都相同,$LIGHTNING_ARTIFACTS_DIR 也指向该目录。**若要保留输出,请通过环境变量将其写入主目录,而不要使用硬编码路径。**作业运行期间在主目录下创建或修改的所有文件都会作为作业工件保存。
import os, joblib
out = os.environ["LIGHTNING_ARTIFACTS_DIR"] # == the source Studio's home path
joblib.dump(model, f"{out}/model.joblib") # a new/changed file under home -> artifact
- **作业结束后,从源 Studio 的
/teamspace/jobs/<job-name>/artifacts/中读取结果。**该路径为只读路径:在作业内部向此路径写入会失败,并报错OSError: [Errno 30] Read-only file system。 - **作业无法更改 Studio 的实时文件系统。**运行结束后,输出不会重新出现在 Studio 的主目录中。
**可通过 teamspace Drive 从任何位置获取工件。**捕获的内容也会显示在 jobs/<job-name>/ 下,因此无需 Studio 即可读取。在 Python 中,job.artifacts_uri 会提供该 lit:// 地址,job.list_artifacts(path, recursive=True) / job.download_artifacts(target_dir, path) 可读取这些内容(多机作业中按机器区分)。在 Shell 中:
lightning cp lit://<owner>/<teamspace>/jobs/<job-name>/model.joblib ./model.joblib # one file
lightning cp -r lit://<owner>/<teamspace>/jobs/<job-name>/outputs/ ./outputs # a folder
**列出并复制特定子文件夹,切勿操作 jobs/<job-name>/ 根目录。**捕获内容可能包含作业的整个主目录,包括点文件和缓存。对根目录运行 lightning ls 可能会卡住,而运行 lightning ls lit://<owner>/<teamspace>/jobs/<job-name>/outputs 会很快返回。在 Python 中,job.list_artifacts("outputs", recursive=True) 会立即返回;job.download_artifacts("outputs", "outputs") 会将作业 outputs/ 中的内容写入本地 ./outputs。删除作业时,此目录树也会一并删除。
**Image(docker)作业不会收集主目录工件,容器内也没有 Lightning 凭据。**容器会获取 LIGHTNING_CLOUD_URL、作业和项目 ID 以及 LIGHTNING_USERNAME,但不会获取 LIGHTNING_API_KEY,因此无法从容器内使用 lightning cp 将文件复制到 Drive。可将少量结果打印到日志中(使用 lightning job logs 读取),通过 path_mappings 挂载输出位置,或在文件必须返回时使用 Studio 作业。
机器
CPU_SMALL、CPU、CPU_X_2/4/8/16、DATA_PREP(_MAX/_ULTRA)、T4_SMALL、T4(_X_2/4/8)、L4(_X_2/4/8)、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(_X_8)。多 GPU 的 _X_N 变体按 N 张 GPU 计费;多机器作业按每台机器的费用 × num_machines 计费。
以上名称截至撰写时有效,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 是机器空闲前预计等待的秒数。使用对应行的机器和账户启动(Job.run(machine=m, cloud=acct, …),或使用通过 cloud=acct 创建的 Studio):关键是该账户支持所选数量的 GPU。SDK 会将 GPU 的不同名称(H200、lit-h200-1、lit-h200-141gb-1)映射到同一台机器。无参数调用 list_machines() 会合并多个账户,却不说明每行属于哪个账户,因此无法据此判断应在哪启动。向用户展示列表前几行及其价格和等待时间:只看排序列表的开头,不看末尾;并且在做其他事情前,先确认第一行符合请求。
优先选择 Lightning 自有账户(lightning-baremetal、lightning-public-prod)。 开发和测试账户可能会列出无法启动的 GPU:区域故障、纯 HTTP 500 错误、no price available,或启动后没有 GPU 的 Studio。wait_time 恰好为 300 或 10800 秒时,这是占位值,不是预估时间。如果某个账户无法启动,请改用下一行,而不是重试。
启动任何 GPU 前都要先询问,并等待用户同意。 告知用户机器、所属账户、每小时价格,以及在截止时间内运行可能产生的最高费用。即使用户指定了 GPU 或给出了预算,也要询问:预算是上限,不代表同意支出。登录前获取报价时,GET /v1/core/accelerators?cloudProvider=<PROVIDER> 无需身份验证(lightning-cost-estimation 技能中提供了各供应商的取值和费用计算方法)。不要臆造目录端点:/v1/accelerators、/v1/accelerator-catalog、/v1/pricing 和 /v1/compute/accelerators 都会返回 code: 5。
启动 GPU 作业前先选择云账户。 默认云账户并不支持所有 GPU 和数量组合(AWS 上没有 1× H200)。如果账户无法满足请求,可能会报错 accelerator … not found for this AWS cluster,无论机器叫什么;但也有按请求配置的 Studio 静默地以 CPU 启动的情况(参见 lightning-studios),所以不要指望一定会明确报错。应使用上述列表中的账户 cluster_id,而不是 Teamspace.cloud_accounts 中显示的名称,例如 Lightning Cloud:使用该名称调用 Studio(cloud=…) 会报错 clusterID Lightning Cloud is invalid。在 CLI 中,使用 GPU 名称并通过 --cloud 指定账户:
lightning job run --name my-job --teamspace owner/teamspace --machine H200 --cloud lightning-baremetal \
--image python:3.12-slim --command "nvidia-smi --query-gpu=name,memory.total --format=csv,noheader"
启动后的第一分钟
大多数作业故障都发生在最初几秒:机器选错、缺少软件包、参数已重命名。作业进入 Running 状态后,先查看日志,再等待;一旦出现第一个 traceback,就停止作业,不要让它继续挂着:
lightning job logs my-job --teamspace owner/teamspace --tail 40 # confirm the GPU line and the first steps
lightning job stop my-job --teamspace owner/teamspace # only if the log shows a crash or the wrong machine
在作业自身的命令开头先检查硬件和目录,例如
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader && pwd && ls && python train.py。
这样日志的前几行就能显示实际分配到的资源,以及脚本是否位于命令的起始目录中。命令在纯 POSIX sh 中运行,而不是 bash:set -o pipefail、[[ … ]] 和数组会在脚本启动前导致失败,因此请将命令限制为用 && 连接的形式。
示例工作流
此技能可处理的提示词:“在 A100 上将此脚本作为批处理作业运行”、“在 lightning 上启动我的 Docker 镜像”、“我的作业为什么失败了——给我看看日志”、“通过 SSH 登录我正在运行的作业”、“通过 SSH 登录我的多机作业中的 rank 1”、“运行 2 节点分布式训练”、“我的训练运行进展到什么程度了?”、“显示此作业的进度条”。
在 GPU 上运行本地脚本并取回其输出文件(使用 studio 作业,因为镜像作业无法自行返回文件)。从 Machines 中的实时列表选择 ACCOUNT 和 MACHINE,并先与用户确认价格:
import time
from lightning_sdk import Job, Machine, Studio
# start_ready, upload_landed, stop_quietly: from the lightning-studios skill (*Set up on CPU, switch to a GPU*),
# with its other helpers and imports
studio = Studio("run-train", teamspace="my-org/my-teamspace", cloud=ACCOUNT, create_ok=True)
# an existing Studio of that name is reused as is, on whatever cloud it was created on
assert studio.cloud_account == ACCOUNT, f"{studio.name} is on {studio.cloud_account}: pick a new name"
try: # the Studio stops on any failure, and once the job starts
start_ready(studio, Machine.CPU) # CPU is enough: it only holds the files
upload_landed(studio, "train.py", "train.py") # the Studio home, the job's working dir; the
# snapshot is taken at Job.run, so it must be there
job = Job.run(name=f"train-{int(time.time())}", machine=MACHINE, studio=studio, # no teamspace=,
cloud=ACCOUNT, # else the default cloud wins and can mismatch the Studio's
command="env -u UV_LIGHTNING_VIRTUALENV_ROOT uv run train.py", # see Gotchas
max_run_attempts=1)
deadline = time.time() + 20 * 60 # the first job from a Studio waits ~5 min for a snapshot
while str(job.status).endswith("Pending"):
if time.time() > deadline:
job.stop()
raise TimeoutError(f"{job.name} still Pending after 20 min")
time.sleep(15)
finally: # the job runs on the snapshot, not the Studio
stop_quietly(studio)
job.wait(interval=15, timeout=2 * 3600, stop_on_timeout=True)
print(job.status, job.total_cost)
job.download_artifacts("outputs", "outputs") # whatever train.py wrote under ./outputs
同一个 Studio 后续启动的作业会复用其快照,更快离开 Pending 状态。
一次性运行(训练、评估、批处理)优先选择作业,而非 Studio。 作业的命令退出后便会停止计费,包括崩溃退出。Studio 上崩溃的运行会让 GPU 持续空闲计费,直到有人发现。例外情况是截止时间紧迫的短时运行:这种情况下,直接在 GPU 上启动 Studio 并设置截止时间使其停止,会比等待快照更快完成(参见 lightning-studios 技能)。在展示计划前先选定一种方式,并在计划中说明,同时注明输出文件的保存位置:作业的输出位于团队空间 Drive 的 jobs/<job-name>/ 下;Studio 的输出位于其主目录中,Studio 停止后仍可下载。不要承诺一种方式却实际运行另一种。
参数扫描:通过一个循环启动多个作业。
for lr in ["1e-3", "3e-4", "1e-4"]:
Job.run(name=f"sweep-lr-{lr}", machine=Machine.T4, studio="exp-1",
command=f"python train.py --lr {lr}", env={"WANDB_RUN": f"lr-{lr}"},
teamspace="my-org/my-teamspace", interruptible=True)
# each writes outputs to home ($LIGHTNING_ARTIFACTS_DIR); read them from the Studio under /teamspace/jobs/<name>/artifacts
实时进度、预计完成时间和挫折
对于值得关注、耗时较长的任务,请向用户展示任务进度、预计完成时间,以及失败带来的代价。平台只会报告任务状态,因此任务会自行输出进度,旁边的 progress.py 会将其转换为状态栏进度条和聊天事件。references/progress.md 详细介绍了每个步骤:行格式、Studio 日志、进度条布局,以及挫折的分类。<SKILL_DIR> 是此技能所在的目录,也就是 Claude Code 加载技能时提供的基础目录。请使用其中的 progress.py;不要在磁盘上搜索其他副本,因为它可能较旧,导致每个进度条都被绘制出来。**对话压缩后,请在下次启动前重新加载此技能。**摘要会保留 progress.py 的调用方法,但不会保留每次运行都需要它这一点,因此后续启动将无人监控。
job ── PROGRESS 450/1000 ──► progress.py watch (background, no tokens)
├─► ~/.local/state/lightning-progress/state/<run>.json ─► status line (terminal)
└─► ~/.local/state/lightning-progress/events.jsonl ────► Monitor (terminal + desktop)
-
让任务输出进度:一旦总步数确定,就输出
PROGRESS <step>/<total>,之后每隔一到几步输出一次;仅由 rank 0 输出,并以结束运行的单位计数(如果由时间预算终止,就按秒计数)。如果任务除了训练还会执行其他操作,请用PROGRESS_PHASE <name> <i>/<n>标记阶段。tqdm 进度条是备用方案。print(f"PROGRESS {step}/{total_steps}", flush=True)对于队列(例如 8 个检查点 × 7 次评估),请在启动脚本中统计整个队列的进度,这样进度条从第一个项目开始就会移动。日志如果只记录当前处理的项目,就没有可供进度条统计的信息。
echo "PROGRESS_PHASE ckpt-376:mmlu 1/56" -
启动轮询器:将其作为后台 Bash 命令运行,并置于 Agent 沙箱之外(SDK 的请求在沙箱内会失败;请让用户批准在沙箱外运行)。任意
python3均可。先记下它输出的运行名称:通常是任务名称;如果有其他工作负载占用该名称,则会附加限定信息。python3 <SKILL_DIR>/progress.py watch train-run-42 --teamspace my-org/my-teamspace对于 Studio 中的工作,请启动任务,使日志以
PROGRESS_EXIT <code>结尾,并改为监视日志文件:watch --studio <name> --log <path>(参见参考文档)。在告知用户运行已开始前,先确认进度条有计数:运行
python3 <SKILL_DIR>/progress.py statusline来绘制进度条。如果任务开始后仍只显示no progress reported yet,说明日志中没有进度条可读取的信息;请修正任务的输出内容。 -
监视事件:
watch输出运行名称后,使用 Monitor 运行python3 <SKILL_DIR>/progress.py events --run <RUN>,将超时时间设为最大值;超时后重新启动(它会从上次停止的位置继续),并在每个事件出现时报告。它只会输出需要回复的内容(停滞、挫折、失败、重新启动、最终状态),并会在运行结束时退出。如果没有状态栏进度条(桌面应用、IDE),请添加--all。 -
如果
watch提示尚未设置状态栏进度条,请单独询问用户一次是否启用:为所有项目启用(推荐)时使用statusline --config --user;仅为此项目启用时使用statusline --config --project-dir <the directory Claude Code was started in>。将输出的代码块合并到它指定的设置文件中;用户现有的状态栏仍会继续运行。 -
将失败的运行重新启动到同一个运行中,以保留历史记录:轮询器会等待 30 分钟。启动修复后的任务,然后运行
python3 <SKILL_DIR>/progress.py watch <new-job> --run <RUN> --note "<what changed>"。如果要放弃,请运行progress.py abandon <RUN>,同样要在沙箱外运行:它会写入进度文件夹,而沙箱无法写入。
原始 API 回退方案
用于 CLI 未封装的内容,主要是精确成本 JSON 和其他原始资源字段。
将列表端点作为普通 GET 请求调用,并在客户端使用 -q 进行切片。 lightning api 会将任何带有 -f/-F 字段且没有 -X 的请求作为 POST 发送(创建调用)。因此,在 /jobs 上使用不带 -X GET 的 -F limit=… 会得到 400 "spec is required";在 /multi-machine-jobs 上则会得到 400 "name is required"。
PROJECT_ID=$(lightning api /v1/memberships | jq -r '.memberships[] | select(.name=="<teamspace>") | .projectId' | head -1)
# list jobs (id + name) — plain GET, no -F
lightning api "/v1/projects/${PROJECT_ID}/jobs" -q '.jobs[] | [.id, .name] | @tsv'
# inspect one job as JSON (status, machine, cost, timestamps) — by JOB ID (job_...), not name
lightning api "/v1/projects/${PROJECT_ID}/jobs/${JOB_ID}"
# list multi-machine jobs — also a plain GET, no -F
lightning api "/v1/projects/${PROJECT_ID}/multi-machine-jobs" -q '.multiMachineJobs[].name'
JOB_ID 是列表调用返回的 job_... ID;各个作业端点对人类可读名称会返回 404。要按名称查找单个作业,请筛选列表。 上次进行实时测试时,/jobs/find 路由返回了 501 Not Implemented。SDK 的 Job("<name>") 会通过 GET .../jobs/find?name=<name> 调用该路由,这种形式或许可用,但此处未经测试。日常使用建议优先使用 CLI:lightning job list --json、lightning job inspect <name>、lightning job logs <name>。
注意事项
- 继承的
DEBUG=1会泄露凭据。 SDK HTTP 诊断会打印Authorization标头,即使只是读取配置也会如此,这就是为什么设置代码块会设置DEBUG=0 LIGHTNING_DEBUG=0。切勿公开此类日志;如果打印出了密钥,请告知用户轮换密钥(头像 → 全局设置 → 密钥)。 - 已保存的 teamspace 可能属于另一个登录账号。 如果无法解析,请对比
lightning auth whoami和lightning api /v1/memberships,并且绝不要自行切换组织:在其他地方仅有一个成员身份,并不意味着可以替代用户指定的组织。请参阅登录了错误的账号。 - 作业在分配机器期间会产生机器费用;在 GPU 上启动作业或设置
num_machines> 1 之前,务必先向用户确认(参见机器),并优先使用wait(..., stop_on_timeout=True),以便停止失控的作业。 - 将无响应的监控视为失败。 没有截止时间的轮询器、只匹配进度行的轮询器,或无法连接 lightning.ai 的轮询器(例如沙盒中的后台命令),在作业崩溃时都会保持静默。为
job.status设置截止时间并轮询,同时处理Failed/Stopped和Completed状态,并确认轮询器会打印第一行。 max_runtime不是时间或费用上限。 它是 DWS 预留时长,仅适用于非 spot 且dws_supported/dws_only的机器,在其他情况下不起作用(已验证在 L40S 运行中无效)。job.wait(timeout=..., stop_on_timeout=True)确实会停止作业,但前提是调用进程仍在运行。因此,不要承诺普通机器上的无人值守时限保障:说明是什么机制在执行限制,并确保有进程持续运行以调用job.stop()。Pending启动缓慢不代表失败,但Pending不代表不计费。 镜像作业在分配到机器前可能会长时间处于Pending/creating状态,这段排队时间免费。配置机器也会报告Pending,且会计费:计费时间为startedAt→stoppedAt,其中包括拉取镜像的时间(参见lightning-cost-estimation),因此在配置过程中停止并非免费。机器列表中的wait_time会预测排队时间。如果在截止时间到达时决定放弃,请依次调用job.stop()和job.delete()。- 不带
-y运行lightning job delete会直接退出而不删除。 在非交互模式下,它会从已关闭的 stdin 读取提示,打印Are you sure you want to delete? [y/N]: Aborted.,并保留作业列表中的作业,继续产生费用。日志中很容易忽略这一失败。 --query和--severity无法筛选所有已完成的作业。 这取决于作业日志的存储位置:如果日志行不在较新的日志存储中,已完成的作业就会回退到保存的日志文件,而该文件无法在服务端筛选。此时 CLI 和 SDK 会报错This job's logs are only available as a saved file ... filter locally,而不是返回空结果,因此请先获取未经筛选的日志,再在本地使用grep。--timestamps在两种情况下都有效。作业仍处于Running状态时,筛选会在服务端执行且正常工作。job inspect没有退出代码或失败消息,因此请读取原始记录的message字段,了解作业失败原因:lightning api "/v1/projects/$PID/jobs" -q '.jobs[] | select(.name=="<name>") | .message'。开始和停止时间位于Job.started_at/Job.stopped_at以及job list --json中。- 工作室作业的 Studio 必须与作业位于同一个 teamspace 和云账号中。
- 在 Python 中,不要在传入
Studio对象时同时传入teamspace=。Job.run(studio=s, teamspace="owner/ts")会引发ValueError: Studio teamspace does not match provided teamspace,即使该 teamspace 正是 Studio 自己的 teamspace 也一样。省略teamspace:作业会从 Studio 获取该值。 - 在工作室作业中运行
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指向了那里并不存在的文件夹(在全新的 Studio 中也会出现相同问题)。请运行env -u UV_LIGHTNING_VIRTUALENV_ROOT uv run …(取消设置所有UV_*变量也有效)。 - 作业会依次经历
Running→Pending→Completed(或Failed),第二次进入Pending并非重试。 这是平台在命令退出后保存作业输出的过程。此时不要停止作业;请使用job.wait()等待,它只会在作业进入终止状态后返回。 job.logs不是字符串。print(job.logs)可以正常工作,但对它进行切片(job.logs[-2000:])会引发TypeError: '_Logs' object is not subscriptable。要查看日志末尾,请使用lightning job logs <name> --tail N。- 已被占用的作业名称会被静默替换,因此请读取
job.name确认名称。 名称在每个 teamspace 中必须唯一:如果名称冲突,平台会使用新名称创建作业,SDK 会警告the job was created as '<new>' instead。省略--name时会自动生成名称。 --machine区分大小写(--machine a100会失败并报错Invalid value for '--machine');请使用上面的准确名称。A100_40GB/A100_80GB 变体仅适用于 SDK(CLI 中隐藏)。job.stop()会阻塞(每 1 秒轮询一次),直到作业进入终止状态。-e/env=的值对 teamspace 中的所有人可见。 不要通过这种方式传递令牌(Hugging Face、W&B、API 密钥);如果已经传递,请告知用户轮换令牌。作业没有 secrets 选项,因此需要令牌的工作应在 Studio 上运行,由用户自行登录(hf auth login),或者选择无需令牌的模型或数据集。- 在更大的 GPU 上测得的内存峰值不能直接套用到更小的 GPU 上。 预热步骤(例如注意力自动调优)和分配器会占用空闲内存:在 H200 上测得的 64 GB 峰值,在 80 GB H100 上仍会耗尽内存,无论是否启用梯度检查点。请在测得该数值的 GPU 上运行,或者先在较小的 GPU 上测试几个步骤,再进行完整运行。
- 每次重新连接时,
job.logs(follow=True)都会重放作业保存的日志行,并且作业运行期间会忽略since。 任何在重新连接后继续跟踪日志的工具,都必须丢弃已看过的行。progress.py会请求timestamps=True并跳过已处理的行;否则,重放的早期进度行会看起来像是进度倒退。 - 直接轮询 Lightning 的 Monitor 在 Claude Code 沙盒中会失败,因为 SDK 和 CLI 的请求无法访问外部网络,而
sleep 60; lightning …这样的命令链也会被拦截。请将网络操作放在一个未沙盒化的progress.py watch中,并让 Monitor 调用progress.py events,后者只读取~/.local/state/lightning-progress/events.jsonl。状态行也会读取同一个文件夹。两者都不需要写入,因此无需将文件夹移到沙盒可写的位置;会随会话一起消失的会话暂存区文件夹会导致进度条空白。对于日志非常频繁的作业,watch --query PROGRESS可以减少流量,但也会隐藏错误行,因此作业停滞时可能无法找到原因。