跳到主要内容

训练一个超过基座模型的金融决策模型

本教程在公开的 EnvLoop Financial Decision Bench 上微调 Qwen/Qwen3.5-4B,并用留出的题目证明效果。你会在花费上限内训练一个 LoRA adapter,把它和基座模型部署在同一个 endpoint 上,再用同样的 550 道验证题给两者打分。第二轮针对最弱的两项能力继续训练。最后一节介绍数据放不进一次请求时,如何从已发布的 Dataset 训练。

训练和推理 endpoint 都会分配付费 GPU 资源。开始前请先读清理资源。

目标和结果​

每道题都是一次决策。模型读取证据(例如公告、参考利率和市场观测值)和明确给出的条件,然后只回答一个有依据的选项 ID,例如 C。题目覆盖 11 项能力:信用、固定收益、投资组合、外汇流动性、期权、能源、条件应用、信息时点、计量解读、官方决策选择、单位与参数映射。每道题都有英文、中文和日文三个版本。

按本教程完成的一次参考运行,在 550 道验证题(每项能力 50 道)上的得分如下。评测时关闭 thinking,temperature 为 0,按选项 ID 精确比对:

模型训练数据准确率
Qwen/Qwen3.5-4B 基座(/base)无25.5%
第一轮 adapter覆盖全部能力的 1,137 条74.7%
第二轮 adapter(从第一轮继续)1,578 条定向数据80.0%

第一轮让每项能力都有提升。第二轮补充能源和单位映射的数据,并少量回放其他能力。能源从 40% 升到 62%,单位映射从 62% 升到 88%,其余九项的平均分没有下降。第二轮的另一次运行总分为 78.2%。完整表格见评测。用同一个脚本在 EnvLoop endpoint 上评测,基座模型为 24.9% 到 25.6%,第一轮 adapter 为 74.7%。从基座模型出发、内联训练一轮,大约能到 74% 到 76%。80% 来自第二轮,它同样是内联提交,但从第一轮的 Checkpoint 继续训练。

验证集是公开的,数据集开发期间也用过。这些数字是基座与微调权重在开发集上的对比。

准备工作​

  1. 安装带 data extra 的 CLI 和 SDK,以及两个脚本用到的库:

    python -m pip install --upgrade "envloop-cli[data]" huggingface_hub pyarrow openai
    el --version

    envloop:// 引用和 checkpoint 下载需要 data extra。如果以前装过 envloop-cli 1.23.0 或 1.24.0,先按安装里的步骤卸载。

  2. 用 el auth login 登录并选择项目,再运行 el auth status --check。详见认证。你在项目中的角色需要是 Developer 或更高。登录会保存一把 Project automation Key,本教程的每一步都只用这一把 Key:查询配方、训练、checkpoint、adapter、endpoint、evaluate.py 的推理调用、清理和发布 Dataset。el auth login --read-only 发放的 Key 不能提交 Job,也不能发布。读取清理 Operation 和运行 el setting cleanup 需要 Key 有资源清理权限。角色为 Developer 或 Admin 时,el auth login 发放的 Key 就有这项权限。在 Console 创建的 Key,除了 Experiment 和 Data 的权限行,还需要 MANAGE → 资源清理的编辑权限和 MANAGE → 用量的读取权限(见创建 Key)。

  3. 把两个教程脚本放进一个空的工作目录:

    mkdir fin-decision && cd fin-decision
    curl -fsSO https://docs.envloop.ai/examples/finance-decision/prepare_data.py
    curl -fsSO https://docs.envloop.ai/examples/finance-decision/evaluate.py

准备数据​

先阅读数据集说明。使用 default 配置,并固定到 v1.0.0 标签。数据集作者编写的内容采用 CC BY 4.0,说明中 LICENSE 和 SOURCE_NOTICES.md 列出的上游来源条款仍然适用。单独的 crypto-nc 配置仅限非商业用途,脚本不会读取它。

数据集的每一行有 messages(一条 system 消息和一条 user 消息)和 answer(一个选项 ID)。训练时,脚本把答案追加为最后一条 assistant 消息,因此 loss 只计算在选项 ID 上。评测时,messages 和 answer 分开保存。

python prepare_data.py --round 1 --out-dir r1

脚本会下载 default 的分片,用仓库里的 SHA256SUMS 校验,然后写出:

文件内容
r1/request.jsonTrain Job 请求,内联了来自 train split 的 1,137 条训练数据,每项能力约 103 条
r1/train_meta.json每条训练数据对应的源行和 base case,第二轮会用到
r1/eval.jsonl来自 validation split 的 550 条,每项能力 50 条
r1/focus.jsonl能源和单位映射各另外 150 条验证题,即第二轮默认的重点能力

一个 base case 是同一道题的三种语言版本。脚本对每个 base case 只取一种语言,三种语言轮流取,并断言训练用到的 base case 不会出现在任一评测文件中。

r1/request.json 的内容如下(records 已缩短):

{
"request_id": "fin-decision-r1-0123456789ab",
"base_model": "Qwen/Qwen3.5-4B",
"recipe": "sft",
"hyperparams": {"learning_rate": 0.0002, "epochs": 2, "lora_rank": 16, "batch_size": 16,
"warmup_ratio": 0.05, "lr_scheduler": "cosine"},
"max_length": 8192,
"enable_thinking": false,
"tags": {"experiment": "fin-decision", "round": "1"},
"records": [
{"messages": [
{"role": "system", "content": "Use the supplied evidence and explicit conditions to select one supported option. Output only its ID."},
{"role": "user", "content": "{\"option_order\":[\"A\",\"B\",\"C\",\"D\",\"E\"],\"question\":{...}}"},
{"role": "assistant", "content": "C"}]}
]
}
  • request_id 只生成一次。用同一个输出目录再次运行脚本时会保留这个 ID,重新提交会返回同一个 Job,不会再启动一次。要用同一个文件启动新 Job,用 el job submit ... --request-id NEW_ID 传入新的 ID,它会覆盖文件里的值。
  • enable_thinking: false 让提示渲染时不带推理段落。推理时也要用同样的设置。
  • 一个 Train 请求内联的 records 最多 6 MiB。脚本在接近这个上限时停止加数据,这样一轮训练也能稳稳落在 Qwen3.5-4B 配方 3 小时的运行时限之内。一轮需要更多数据时,见数据更大时。

第一轮训练​

先确认项目提供 Qwen3.5-4B 的通用配方,并读取它的限制:

python - <<'PY'
import json
from envloop import Client

with Client.remote() as client:
models = client.training_jobs.models()
recipe = next(item for item in models["items"]
if item["base_model"] == "Qwen/Qwen3.5-4B" and item["recipe"] == "sft")
print(json.dumps({key: recipe[key] for key in ("model_revision", "limits", "gpu")}, indent=2))
print(json.dumps(recipe["features"]["parent_checkpoint"], indent=2))
PY

limits.timeout_seconds 是单个 Trial 的运行时限,从启动开始计算。limits.max_trial_cost_usd 是单个 Trial 最多可能产生的花费。features.parent_checkpoint 列出从已有 Checkpoint 继续训练的规则。如果找不到这个配方,提交前请联系支持。

带上花费上限提交请求:

el job submit --type train --spec r1/request.json --max-cost-usd YOUR_CAP_USD > r1/submit.json

--max-cost-usd 限制整个 Job 的 GPU 用时花费,请设为不低于 limits.max_trial_cost_usd 的值。只有上限足以覆盖一个 Trial 的完整运行时限,Trial 才会启动,上限更低时什么都不会启动。详见 Train Job 预算。

响应中包含 job_id。1,137 条数据、2 个 epoch、batch size 16,Job 会运行 ceil(2 × 1137 ÷ 16) = 143 步,可在 r1/submit.json 的 hyperparameters.max_steps 中核对。如果没收到响应,用同一个文件重新运行同一条命令,相同的 request_id 会返回已有的 Job。

观察训练​

el job metrics YOUR_R1_JOB_ID --follow

--follow 每记录一步就打印一行,包括 loss、学习率、梯度范数、epoch、已处理 token 数和每步秒数。Job 结束时自动退出,按 Ctrl-C 只是停止读取。第一步要等 GPU 分配、启动和模型加载之后才出现,参考运行中约 5 分钟。之后训练每步 20 到 30 秒,一轮在提交后 60 到 80 分钟完成。

另开一个终端:

el job get YOUR_R1_JOB_ID > r1/job.json
el job log YOUR_R1_JOB_ID --source all --tail 50

el job get 在 stderr 打印 Training progress: 摘要和相对上限的花费,在 stdout 输出 Job JSON。查看 status、phase 和 training.progress。第一步出现之前,phase 先是 PLANNED(已接受,还没交给 provider),然后是 LAUNCHING(正在等待并准备 GPU)。正常的运行里,loss 在最初几步迅速下降,随后趋于平稳。status 为 completed 即训练完成。cancelled 且错误为 BUDGET_EXCEEDED 表示达到了花费上限,failed 且错误为 TRAINING_TIMEOUT 表示达到了运行时限,这两种情况都不会登记 Checkpoint。详见监控 Train Job。

下载 Checkpoint​

el checkpoint list --job-id YOUR_R1_JOB_ID --json > r1/checkpoints.json

使用最终保存的那一项,它的 checkpoint_name 和 checkpoint_id 都是 adapter-final。记下它的两个值:

  • resource_id(cp_…)是 Checkpoint ID,下载和删除用它。
  • uri(envloop://model/envloop/Qwen3.5-4B@lora-sft-r16-…)是模型引用,注册 adapter 和下一轮训练用它。

下载到一个新目录,目录是位置参数:

el checkpoint download YOUR_R1_CHECKPOINT_ID r1/checkpoint > r1/download.json
shasum -a 256 r1/checkpoint/adapter-final/adapter_model.safetensors

下载命令会按登记的元数据核对每个文件的大小和 SHA-256,不一致就报错,r1/download.json 记录了核对过的文件。shasum 是一次独立核对:把输出和同一文件记录的 sha256 比较。这条命令也接受 uri。

部署第一轮​

找到基座模型对应的 serving profile:

el job serving-profile list --json

使用 qwen35-4b@1:它的 base_model 是 Qwen/Qwen3.5-4B,lora.max_rank 能容纳 rank 16。把 Checkpoint 注册为 adapter,保存 r1/adapter.json:

{
"request_id": "fin-decision-adapter-r1-YOUR_UNIQUE_SUFFIX",
"serving_profile": "qwen35-4b@1",
"source": {"type": "train_checkpoint", "uri": "YOUR_R1_CHECKPOINT_URI"}
}
el job adapter register --file r1/adapter.json > r1/adapter-receipt.json

确认 status: READY,并记下 adapter_id。adapter 本身不会运行任何东西。接下来准备 endpoint,保存 r1/inference.json:

{
"request_id": "fin-decision-eval-r1-YOUR_UNIQUE_SUFFIX",
"spec": {
"name": "fin-eval",
"serving_profile": "qwen35-4b@1",
"adapters": [{"alias": "r1", "adapter_id": "YOUR_R1_ADAPTER_ID"}],
"scaling": {"min_replicas": 1, "max_replicas": 1, "scale_to_zero_after_seconds": 600}
}
}
endpoint 按运行时长计费

Inference Job 从分配 GPU 起一直占用它,直到被停止,期间有没有请求都一样,加载模型的时间也计算在内。min_replicas: 1 时副本立即启动,一直运行到你停止 Job。准备好评测再提交,评分一结束就停止(见清理资源)。提交时还会检查工作区余额能否支付每个启动副本一小时的费用,不够时返回 402 INSUFFICIENT_BALANCE,details 里写明金额。

el job submit --type inference --spec r1/inference.json > r1/inference-submit.json
el job get YOUR_INFERENCE_JOB_ID

重复执行 el job get,直到 inference.service_status 变为 READY;PROVISIONING 表示 worker 还在加载。副本开始服务后,el job event YOUR_INFERENCE_JOB_ID 里也会出现一条 READY 事件。参考运行中,endpoint 在提交后 6 到 11 分钟就绪。如果 PROVISIONING 持续时间远超 profile 的 cold_start_budget_seconds,查看 el job event YOUR_INFERENCE_JOB_ID --json 并停止 Job,不要一直等。

记下响应中的 inference.model,格式为 YOUR_WORKSPACE_ID/fin-eval。同一个 Job 同时提供两个模型:

  • YOUR_WORKSPACE_ID/fin-eval/base 是基座模型,响应中的 model 为 qwen35-4b。
  • YOUR_WORKSPACE_ID/fin-eval/r1 是你的 adapter,响应中的 model 为 r1。

不带后缀时,Job 调用的是第一个 adapter,不是基座模型。详见模型名称。

评测​

在同样的 550 条数据上给基座模型和 adapter 打分:

python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --input r1/eval.jsonl --out-dir r1/results

evaluate.py 使用 el auth login 保存的连接。它对每一行发送 messages,关闭 thinking,temperature 为 0,最多生成 16 个 token。只有去掉首尾空白后的输出与选项 ID 完全相同才算答对。脚本每次调用都检查响应中的 model,如果答题的不是指定的权重就立即停止。冷启动时会返回 503 ENDPOINT_COLD_STARTING 和 Retry-After,脚本会在 --deadline-seconds 内等待并重试。8 路并发时,每个模型跑 550 条约需一分钟。原始输出保存在 r1/results/base.jsonl 和 r1/results/r1.jsonl。

脚本打印一张 Markdown 表,列出每项能力、每种语言和总体的准确率(%)。参考运行的结果如下,第二轮的数据来自下一节:

能力base第一轮第二轮
credit208280
declared_condition_application47678
energy244062
fixed_income208084
fx_liquidity147680
information_timing_scope50100100
measurement_interpretation427884
official_decision_selection68100100
options66468
portfolio26456
unit_and_parameter_mapping306288
总计(550)25.574.780.0

每项能力只有 50 题,一题就是 2 个百分点。单项能力的小幅变化(例如第二轮的 portfolio)在噪声范围内。同一个模型测两次,也可能有几题答案不同。

如果不马上进行第二轮,现在就去清理资源。

第二轮:补强弱项​

参考运行中,第一轮之后能源(40%)和单位映射(62%)明显低于其他能力。从你自己的第一轮结果表中挑出最弱的两项能力,每项用一个 --focus 传入。第二轮从第一轮的权重继续,用这些能力的数据训练,同时给其他每项能力回放 30 条,防止它们退步。

python prepare_data.py --round 2 --out-dir r2 --previous r1/train_meta.json \
--parent-checkpoint YOUR_R1_CHECKPOINT_URI \
--focus energy --focus unit_and_parameter_mapping

不加 --focus 时,脚本使用能源和单位映射。题目较短的那项重点能力最多取 1,000 条,另一项用满 6 MiB 的剩余空间。按参考运行的重点能力,r2/request.json 共 1,578 条:单位映射 1,000 条,能源 308 条,其他九项能力各 30 条。能源的题目很长,所以放得少。没有一条复用第一轮的 base case,也没有一条出现在 eval.jsonl 或 focus.jsonl 中。r2/focus.jsonl 包含每项重点能力各 150 条验证题。请求使用新的 request_id,设置与第一轮相同,并把第一轮 Checkpoint 的 uri 填入 parent_checkpoint。

el job submit --type train --spec r2/request.json --max-cost-usd YOUR_CAP_USD > r2/submit.json
el job metrics YOUR_R2_JOB_ID --follow

父 Checkpoint 必须来自同一项目中已成功的 Train Job,基座模型、配方和 lora_rank 都要相同。新 Job 加载它的 adapter 权重,然后重新开始优化器和学习率调度,父 Checkpoint 本身不会改变。从第一条 loss 就能看出父权重已加载:参考运行中,第一轮从 4.0 左右开始,第二轮从 0.1 左右开始。第二轮运行 198 步,提交后约一小时完成。

下载第二轮的 Checkpoint,用新的 request_id 把它注册为第二个 adapter。然后提交一个同时提供两轮模型的 endpoint,使用新的 request_id,并写 "adapters": [{"alias": "r1", "adapter_id": "YOUR_R1_ADAPTER_ID"}, {"alias": "r2", "adapter_id": "YOUR_R2_ADAPTER_ID"}]。在两个评测文件上给三个模型打分:

python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --target r2=r2 --input r1/eval.jsonl --out-dir r2/results
python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --target r2=r2 --input r2/focus.jsonl --out-dir r2/focus-results

第二轮没有用 300 条 focus 题训练过。在这些题上,参考运行的能源从 36% 升到 65%,单位映射从 68% 升到 93%,合计从 52.0% 升到 79.0%。在 550 条题上,总分从 74.7% 升到 80.0%,其余九项能力的平均分为 81.1%,第一轮之后是 80.0%。能源仍是最弱的一项,中文题尤其明显。第三轮可以把能源作为主要数据。

第二轮各项能力的提升在不同运行之间有差异。本教程的另一次运行使用同样的重点能力,总分为 78.2%。单位映射再次从 60% 升到 94%,但能源在 550 条题上从 38% 变为 34%,在 focus 题上从 36.7% 变为 39.3%。

回放数据不能省。另一次运行从基座模型出发,在一轮里同时放入定向数据和每项能力仅 60 条的回放。单位映射达到 94%,能源达到 56%,但其余九项能力的平均分降到 76.4%,计量解读降到 58%。计量解读的题目很短,60 条带来的训练信号很少,所以回放要按 token 量规划,不要按条数平均分配。

清理资源​

按以下顺序释放资源。

  1. 评分一结束就停止 endpoint:

    el job stop YOUR_INFERENCE_JOB_ID
    el job get YOUR_INFERENCE_JOB_ID

    重复执行 el job get,直到 inference.service_status 为 DELETED、inference.cleanup_confirmed 为 true。这时 endpoint 的 GPU 已经释放。如果还想跟踪释放 Operation,用 inference.cleanup_operation_id 运行 el setting operation wait,它必须以 SUCCEEDED 结束,且没有错误。读取 Operation 需要 Key 有资源清理权限(见准备工作)。

  2. 没有 Job 使用 adapter 后,删除它们:

    el job adapter delete YOUR_R1_ADAPTER_ID --wait
    el job adapter delete YOUR_R2_ADAPTER_ID --wait
    el job adapter list

    --wait 会跟踪删除 Operation,需要同样的权限。没有这项权限时,--wait 返回 403,但删除仍会进行。已删除的 adapter 不再出现在 el job adapter list 中。

  3. 确认 Train Job 已释放计算资源:

    el job get YOUR_R1_JOB_ID
    el job get YOUR_R2_JOB_ID

    每个 Job 都必须显示 training.cleanup_confirmed: true,el job log JOB_ID --source system 中有 Resource release confirmed.。Job 的状态、日志和用量仍可读取。

    有资源清理权限时,还可以为两个 Job 记录一个清理 Operation。把 Train Job ID 写入 cleanup.json:

    [{"kind": "job", "id": "YOUR_R1_JOB_ID"}, {"kind": "job", "id": "YOUR_R2_JOB_ID"}]
    el setting cleanup --file cleanup.json --request-id fin-decision-cleanup-YOUR_UNIQUE_SUFFIX --wait > cleanup-receipt.json
    el setting operation get YOUR_CLEANUP_OPERATION_ID

    使用 cleanup-receipt.json 中的 operation_id。Operation 必须为 SUCCEEDED、errors 为空,且两个 Job 都出现在 compute_released 中。

  4. 决定保留哪些资源。清理后 Checkpoint 会保留。要删除 Checkpoint,先下载需要的文件,再用它的 cp_… ID 运行 el checkpoint delete YOUR_CHECKPOINT_ID --wait。

数据更大时:从 Dataset 训练​

上文两轮都能内联提交,因为第二轮是从第一轮继续训练的。如果想从基座模型出发、一轮训完两轮的全部 2,715 条数据,大约需要 12.7 MB,就必须用 Dataset。一轮需要超过 6 MiB 的数据时,可以把数据一次性发布为项目中的 record Dataset,提交时引用它,不再发送 records。一个 record Dataset split 最多 10,000 行、64 MiB。发布需要 datasets.write,项目角色为 Developer 或 Admin 时,el auth login 发放的 Key 就有这项权限。详见从 Dataset 训练。

  1. 把数据写成 record Dataset。加上 --dataset 后,脚本会额外写出 r1/dataset/files/train/records.jsonl 和 r1/dataset/manifest.json:

    python prepare_data.py --round 1 --out-dir r1 --dataset YOUR_NAMESPACE/fin-decision-r1@1.0.0

    每一行保留 id、ability 和 language 便于追溯,只有 messages 是训练输入。manifest 的 source 写的是 Hugging Face 数据集,以及 v1.0.0 对应的完整 revision commit。source 不接受 v1.0.0 这样的标签,所以脚本记录的是 40 位 commit。EnvLoop 只把 source 当作来源记录,只核对你上传的文件。身份字段放在 manifest 的 dataset 对象里:

    {
    "schema": "envloop.dataset-records/v2",
    "dataset": {"namespace": "YOUR_NAMESPACE", "name": "fin-decision-r1", "version": "1.0.0"},
    "config": "default",
    "splits": {"train": "train/records.jsonl"},
    "format": "jsonl",
    "files": [{"path": "train/records.jsonl", "size": 6333759, "sha256": "FILE_SHA256"}],
    "source": {"repository": "https://huggingface.co/datasets/EnvLoop/financial-decision-bench",
    "commit": "FULL_40_CHARACTER_REVISION_COMMIT"}
    }
  2. 先校验,再发布。dry run 会检查 manifest、文件大小和哈希以及你的权限,不会创建任何资源:

    el data create --type dataset \
    --ref 'envloop://datasets/YOUR_NAMESPACE/fin-decision-r1@1.0.0' \
    --manifest r1/dataset/manifest.json --directory r1/dataset/files --dry-run --json
    el data create --type dataset \
    --ref 'envloop://datasets/YOUR_NAMESPACE/fin-decision-r1@1.0.0' \
    --manifest r1/dataset/manifest.json --directory r1/dataset/files --json > r1/dataset.json

    超过 8 MiB 的文件会分片上传,支持续传。上传中断时,用同样的文件重新运行同一条命令即可。已发布的版本不可修改,要改数据就发布新版本。

  3. 提交。从请求中去掉 records,换一个新的 request_id,并指定 Dataset 和 split:

    python - <<'PY'
    import json
    import uuid
    from pathlib import Path

    request = json.loads(Path("r1/request.json").read_text())
    del request["records"]
    request["request_id"] = "fin-decision-r1-dataset-" + uuid.uuid4().hex[:12]
    Path("r1/request-dataset.json").write_text(json.dumps(request, indent=2) + "\n")
    PY
    el job submit --type train --spec r1/request-dataset.json \
    --dataset YOUR_NAMESPACE/fin-decision-r1@1.0.0 --split train \
    --max-cost-usd YOUR_CAP_USD > r1/submit.json

    平台在提交时读取这个 split,像内联 records 一样逐行校验,并把内容和 Job 一起冻结。Job 的 dataset 记录了版本的 manifest_sha256、split 文件的 sha256 和 row_count。之后的监控、Checkpoint、部署和评测都与上文相同。

    参考运行中,一个包含两轮共 2,715 行的 Dataset 从基座模型训练 340 步,用时约两个半小时。在同样的 550 道题上,它得分 79.5%,基座模型为 25.8%。

常见问题​

现象原因和处理
prepare_data.py 报 “over 6 MiB”数据放不进一次请求。改为从 Dataset 训练,或用 parent_checkpoint 把数据分到多轮。
提交在接近 6 MiB 时失败请求体上限是 6 MiB,records 外层的 JSON 也计算在内。用脚本重新生成请求,不要手工加数据。
Job 在 GPU 启动前以 cancelled 和 BUDGET_EXCEEDED 结束上限低于 limits.max_trial_cost_usd。用新的 request_id 和更高的上限重新提交。同一个 request_id 换了上限会返回 TRAIN_JOB_IDEMPOTENCY_CONFLICT。
提交 endpoint 返回 402 INSUFFICIENT_BALANCE工作区余额不够副本运行一小时。错误信息和 details.required_usd 给出所需金额(见提交 Inference Job)。请有账单权限的工作区成员在 Console 中补充额度,然后用同一个 request_id 重新提交。被拒绝的提交不会创建 Job,也不占用 GPU。
el job metrics 几分钟都没有数据第一步之前要经过分配、启动和模型加载。用 el job get 查看 status 和 phase,用 el job log 查看启动日志。PLANNED 和 LAUNCHING 表示 Job 还在等 GPU。
Job 以 failed 和 EXECUTION_QUEUE_TIMEOUT 结束Job 开始前等待的时间超过了排队上限。没有运行任何内容,也没有分配 GPU,所以不产生费用。用新的 request ID 重新提交:el job submit --type train --spec r1/request.json --request-id NEW_ID --max-cost-usd YOUR_CAP_USD。prepare_data.py 有意保留旧的 request_id,所以重新运行脚本会返回那个失败的 Job。
最后一步之后 Job 长时间停在 running保存最终 Checkpoint 和释放 GPU 大约需要一分钟。用 el job log JOB_ID --source system --tail 50 查看系统日志。如果最后一步过去 15 分钟后 el checkpoint list --job-id JOB_ID 仍然没有 Checkpoint,带上 Job ID 联系支持。
用 cp_… 还是 uri?parent_checkpoint 和注册 adapter 用 uri。el checkpoint download 两者都可以。el checkpoint delete 用 cp_…。
el setting cleanup 或 el setting operation wait 返回 403Key 没有资源清理权限。改用 el job get 确认释放(见清理资源),或使用角色为 Developer 或 Admin 时 el auth login 发放的 Key。
el setting cleanup 返回 422 INVALID_CLEANUP_RESOURCES错误信息会指出是哪一条、哪个字段。Train Job 的条目写成 {"kind": "job", "id": "JOB_ID"},其他可选的 kind 是 endpoint、adapter 和 checkpoint。
注册 adapter 返回 409 ADAPTER_BASE_MISMATCHprofile 的基座模型和 Checkpoint 不同。Qwen/Qwen3.5-4B 用 qwen35-4b@1。
推理返回 503 ENDPOINT_COLD_STARTINGworker 还在加载。等到 READY;evaluate.py 会按 Retry-After 重试。
基座和 adapter 得分一样检查结果文件里的 served 字段。不带后缀时调用的是第一个 adapter,基座模型要用 /base。
输出很长、准确率接近 0thinking 没有关。像训练时一样发送 "chat_template_kwargs": {"enable_thinking": false}。
el data create 返回 403 DATA_ACCESS_REQUIRED错误信息会写明缺少的权限,例如 datasets.write。可能是 Key 来自 el auth login --read-only,或签发时就没有这项权限,或你的角色是 Viewer。Key 签发后不会再增加权限,重试没有用。请不带 --read-only 重新运行 el auth login(需要 Developer 或 Admin 角色),并撤销旧 Key,详见ANS 数据管理。
el data create 返回 FEATURE_NOT_INSTALLED安装 envloop-cli[data](见安装)。
DATA_PUBLICATION_INVALIDmanifest 和文件不一致,身份字段没有放在 dataset 对象里,或者 source.commit 填了标签、分支或短哈希。重新运行 prepare_data.py,不要手工改文件。
TRAINING_DATA_TOO_LARGE 或 TRAINING_DATA_UNAVAILABLE一个 split 最多 10,000 行、64 MiB。Dataset 必须是本项目已发布的 record Dataset,版本和 split 都要存在。
旧教程里的命令失败旧的 /v1/job/... 路由返回 404,请改用 /v1/jobs/... 和 /v1/training-models。参数名是 --timeout-seconds,不是 --timeout-sec。以 el COMMAND --help 为准。

接下来可以阅读:配方参数中的训练选项,部署和调用训练好的模型中的 endpoint 生命周期,以及监控 Train Job 中的逐步指标。