Skip to main content

Train a financial decision model that beats its base model

This tutorial fine-tunes Qwen/Qwen3.5-4B on the public EnvLoop Financial Decision Bench and proves the result on held-out questions. You train a LoRA adapter under a spend cap, deploy it next to its base model, and score both on the same 550 validation questions. A second round then targets the two weakest abilities. The last section shows how to train from a published Dataset when your data no longer fits in one request.

Training and the inference endpoint allocate paid GPU resources. Read Clean up before you start.

What you will build​

Each question is a decision. The model reads evidence, such as filings, reference rates and market observations, together with explicit conditions. It must answer with exactly one supported option ID, such as C. The 11 abilities cover credit, fixed income, portfolios, FX liquidity, options, energy, condition application, information timing, measurement interpretation, official decision selection, and unit and parameter mapping. Every question exists in English, Chinese and Japanese.

A reference run of this tutorial scored the 550 validation questions (50 per ability) with thinking off, temperature 0 and exact option-ID matching:

ModelTraining rowsAccuracy
Qwen/Qwen3.5-4B base (/base)none25.5%
Round 1 adapter1,137 rows of all abilities74.7%
Round 2 adapter, continued from round 11,578 targeted rows80.0%

Round 1 raises every ability. Round 2 adds energy and unit-mapping rows plus a small replay of the other abilities. It lifts energy from 40% to 62% and unit mapping from 62% to 88%, and the other nine abilities keep their average. A second run of round 2 scored 78.2% in total. The full table is in Evaluate. Scored on an EnvLoop endpoint with the same script, the base model gave 24.9% to 25.6% and the round 1 adapter 74.7%. One inline round from the base model lands around 74% to 76%. The 80% comes from the second round, which is also inline but continues from the round 1 Checkpoint.

The validation split is public and was used while the dataset was developed. Read these numbers as a development comparison of base and fine-tuned weights.

Before you start​

  1. Install the CLI and SDK with the data extra, plus the libraries the two scripts use:

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

    The data extra is needed for envloop:// references and checkpoint downloads. If you once installed envloop-cli 1.23.0 or 1.24.0, follow the removal steps in Installation first.

  2. Sign in with el auth login, choose the project, then run el auth status --check. See Authentication. Your project role must be Developer or higher. The login saves a Project automation Key, and that one Key runs every step of this tutorial: discovery, training, checkpoints, adapters, the endpoint, the inference calls of evaluate.py, cleanup and publishing a Dataset. A Key from el auth login --read-only cannot submit or publish. Reading cleanup Operations and running el setting cleanup need the Key's Resource cleanup permission. A Key from el auth login has it when your role is Developer or Admin. A Key created in the Console needs Edit on MANAGE → Resource cleanup and Read on MANAGE → Usage in addition to the Experiment and Data rows (Create a key).

  3. Put the two tutorial scripts in an empty working directory:

    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

Prepare the data​

Read the dataset card first. Use the default configuration, pinned to the v1.0.0 tag. Its authored content is CC BY 4.0, and the upstream source terms in the card's LICENSE and SOURCE_NOTICES.md still apply. The separate crypto-nc configuration is noncommercial only, and the script never reads it.

Each dataset row has messages, one system and one user message, and answer, one option ID. For training, the script appends the answer as the final assistant message, so the loss covers only the option ID. For evaluation, it keeps messages and answer apart.

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

The script downloads the default shards, checks them against the repository's SHA256SUMS, and writes:

FileContent
r1/request.jsonThe Train Job request, with 1,137 training rows from the train split inline, about 103 per ability
r1/train_meta.jsonThe source row and base case of every training row, used by round 2
r1/eval.jsonl550 rows from the validation split, 50 per ability
r1/focus.jsonl150 more validation rows each of energy and unit mapping, the default round 2 focus

A base case is one question translated into three languages. The script takes one language per base case, rotating through the three. It asserts that no training base case appears in either evaluation file.

r1/request.json looks like this, with the records shortened:

{
"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 is generated once. If you run the script again with the same output directory, it keeps the ID, so a resubmission returns the same Job instead of starting a second one. To start a new Job from the same file, pass a new ID with el job submit ... --request-id NEW_ID; it overrides the file.
  • enable_thinking: false renders the prompts without a reasoning section. Send the same setting at inference.
  • A Train request carries at most 6 MiB of inline records. The script stops adding rows just below that, which also keeps one round well inside the 3-hour run-time limit of the Qwen3.5-4B recipe. For more rows per round, see When your data is larger.

Train round 1​

Check that your project offers the general Qwen3.5-4B recipe, and read its limits:

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 is the run-time limit of one Trial, counted from launch. limits.max_trial_cost_usd is the most one Trial can cost. features.parent_checkpoint lists the rules for continuing from an earlier Checkpoint. If the recipe is missing, contact support before submitting.

Submit the request with a spend cap:

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

--max-cost-usd caps the GPU time of the whole Job. Set it at or above limits.max_trial_cost_usd. A Trial starts only when the cap covers its whole run-time limit, so a lower cap starts nothing. See Train Job budget.

The response contains job_id. With 1,137 rows, 2 epochs and batch size 16, the Job runs ceil(2 × 1137 ÷ 16) = 143 steps; hyperparameters.max_steps in r1/submit.json confirms it. If the response is lost, run the same command with the same file again. The same request_id returns the existing Job.

Watch training​

el job metrics YOUR_R1_JOB_ID --follow

--follow prints one row per recorded step, with loss, learning rate, gradient norm, epoch, tokens seen and seconds per step. It exits when the Job ends, and Ctrl-C only stops reading. The first step appears after GPU allocation, startup and model loading, which took about 5 minutes in the reference runs. Training then ran at 20 to 30 seconds per step, and a round finished 60 to 80 minutes after submission.

In another terminal:

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

el job get prints a Training progress: summary and the spend against your cap on stderr, and the Job JSON on stdout. Check status, phase and training.progress. Before the first step, phase is PLANNED (accepted, not yet released to a provider) and then LAUNCHING (waiting for a GPU and preparing it). In a healthy run the loss drops in the first steps and then flattens. The Job is done when status is completed. A cancelled Job with BUDGET_EXCEEDED reached its cap, and a failed Job with TRAINING_TIMEOUT reached the run-time limit; neither registers a Checkpoint. See Monitor Train Job.

Download the Checkpoint​

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

Use the final save: its checkpoint_name and checkpoint_id are both adapter-final. Keep two of its values:

  • resource_id (cp_…) is the Checkpoint ID for download and delete.
  • uri (envloop://model/envloop/Qwen3.5-4B@lora-sft-r16-…) is the model reference for adapters and later rounds.

Download into a new directory. The directory is a positional argument:

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

The download verifies the size and SHA-256 of every file against the registered metadata and fails on a mismatch; r1/download.json records the files it checked. shasum is an independent check: compare its output with the sha256 recorded for the same file. The command also accepts the uri.

Deploy round 1​

Find the serving profile for the base model:

el job serving-profile list --json

Use qwen35-4b@1. Its base_model is Qwen/Qwen3.5-4B, and its lora.max_rank covers rank 16. Register the Checkpoint as an adapter. Save 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

Check status: READY and keep adapter_id. An adapter does not run anything by itself. Now prepare the endpoint. Save 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}
}
}
Endpoints are billed for the time they run

An Inference Job holds a GPU from allocation until it stops, whether or not you send requests, and the time spent loading the model counts. With min_replicas: 1 the replica starts right away and stays up until you stop the Job. Submit it only when you are ready to evaluate, and stop it as soon as scoring finishes (Clean up). Submission also checks that the workspace balance can cover one hour of each starting replica; if not, it returns 402 INSUFFICIENT_BALANCE with the amount in details.

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

Repeat el job get until inference.service_status is READY; PROVISIONING means the worker is still loading. el job event YOUR_INFERENCE_JOB_ID also lists a READY event once the replica serves. In the reference runs the endpoint was ready 6 to 11 minutes after submission. If it stays in PROVISIONING much longer than the profile's cold_start_budget_seconds, read el job event YOUR_INFERENCE_JOB_ID --json and stop the Job instead of waiting.

Keep inference.model from the response. It has the form YOUR_WORKSPACE_ID/fin-eval, and one Job serves both models:

  • YOUR_WORKSPACE_ID/fin-eval/base is the base model. The response's model is qwen35-4b.
  • YOUR_WORKSPACE_ID/fin-eval/r1 is your adapter. The response's model is r1.

Without a suffix the Job serves the first adapter, not the base model. See model names.

Evaluate​

Score the base model and the adapter on the same 550 rows:

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 uses the connection saved by el auth login. For every row it sends messages with thinking off, temperature 0 and at most 16 new tokens. An answer counts only when the stripped output equals the option ID. The script checks the response's model on every call and stops if other weights answered. A cold start returns 503 ENDPOINT_COLD_STARTING with Retry-After, and the script waits and retries within --deadline-seconds. With 8 parallel requests, 550 rows take about a minute per model. The raw outputs are saved in r1/results/base.jsonl and r1/results/r1.jsonl.

The script prints a Markdown table of accuracy (%) per ability, per language and in total. The reference run, including round 2 from the next section:

Abilitybaseround 1round 2
credit208280
declared_condition_application47678
energy244062
fixed_income208084
fx_liquidity147680
information_timing_scope50100100
measurement_interpretation427884
official_decision_selection68100100
options66468
portfolio26456
unit_and_parameter_mapping306288
Total (550)25.574.780.0

Each ability has 50 questions, so one question is 2 points. Small changes in one ability, such as portfolio in round 2, are within noise. Running the same model twice can also change a few answers.

If you do not continue with round 2 right away, clean up now.

Round 2: target the weak abilities​

In the reference run, round 1 left energy (40%) and unit mapping (62%) far below the other abilities. Pick the two weakest abilities from your round 1 table and pass each one with --focus. Round 2 continues from the round 1 weights with rows of those abilities. It also replays 30 rows of each other ability so they do not regress.

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

Without --focus, the script uses energy and unit mapping. The focus ability with the shorter rows gets up to 1,000 rows, and the other one fills the rest of the 6 MiB. For the reference focus, r2/request.json has 1,578 rows: 1,000 unit mapping, 308 energy, and 30 of each of the other nine abilities. Energy rows are long, so fewer of them fit. No row reuses a round 1 base case, and none appears in eval.jsonl or focus.jsonl. r2/focus.jsonl holds 150 validation rows of each focus ability. The request has a new request_id, the same settings as round 1, and the round 1 Checkpoint uri as 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

The parent must be a succeeded Train Job of the same project, with the same base model, recipe and lora_rank. The new Job loads its adapter weights, then starts a fresh optimizer and learning-rate schedule. The parent Checkpoint does not change. The first recorded loss shows that the parent was loaded: in the reference run, round 1 started near 4.0 and round 2 near 0.1. Round 2 ran 198 steps and finished about an hour after submission.

Download the round 2 Checkpoint and register it as a second adapter with its own request_id. Then submit one endpoint that serves both rounds, with a new request_id and "adapters": [{"alias": "r1", "adapter_id": "YOUR_R1_ADAPTER_ID"}, {"alias": "r2", "adapter_id": "YOUR_R2_ADAPTER_ID"}]. Score all three models on both evaluation files:

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

Round 2 never trained on the 300 focus rows. On them, the reference run moved energy from 36% to 65% and unit mapping from 68% to 93%, a total of 52.0% to 79.0%. On the 550 rows the total rose from 74.7% to 80.0%, and the other nine abilities averaged 81.1%, against 80.0% after round 1. Energy remains the weakest ability, mostly in Chinese. A third round could make energy the main part of its rows.

Per-ability gains in round 2 vary between runs. A second run of this tutorial, with the same focus, scored 78.2% in total. Unit mapping rose from 60% to 94% again, but energy went from 38% to 34% on the 550 rows and from 36.7% to 39.3% on the focus rows.

The replay rows matter. Another run put targeted rows and only 60 replay rows per ability into a single round from the base model. Unit mapping reached 94% and energy 56%, but the other nine abilities fell to 76.4% on average, and measurement interpretation to 58%. Its rows are short, so 60 of them carry little training signal; plan replay by tokens, not by row count.

Clean up​

Release resources in this order.

  1. Stop the endpoint as soon as scoring ends:

    el job stop YOUR_INFERENCE_JOB_ID
    el job get YOUR_INFERENCE_JOB_ID

    Repeat el job get until inference.service_status is DELETED and inference.cleanup_confirmed is true. The endpoint's GPU is then released. To follow the release Operation as well, run el setting operation wait with inference.cleanup_operation_id; it must end SUCCEEDED with no errors. Reading Operations needs the Key's Resource cleanup permission (Before you start).

  2. Delete the adapters once no Job uses them:

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

    --wait follows the deletion Operation, which needs the same permission. Without it, --wait returns 403 although the deletion goes ahead. A deleted adapter no longer appears in el job adapter list.

  3. Confirm that the Train Jobs released their compute:

    el job get YOUR_R1_JOB_ID
    el job get YOUR_R2_JOB_ID

    Each Job must show training.cleanup_confirmed: true, and el job log JOB_ID --source system shows Resource release confirmed. The Jobs' status, logs and usage stay readable.

    With the Resource cleanup permission, you can also record one cleanup Operation for both Jobs. Save cleanup.json with your Train Job IDs:

    [{"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

    Use the operation_id from cleanup-receipt.json. The Operation must be SUCCEEDED with empty errors, with both Jobs in compute_released.

  4. Decide what to keep. Checkpoints are retained after cleanup. To remove a Checkpoint, first download what you need, then run el checkpoint delete YOUR_CHECKPOINT_ID --wait with its cp_… ID.

When your data is larger: train from a Dataset​

Both rounds above fit inline because round 2 continues from round 1. Training everything in one round from the base model, all 2,715 rows of both rounds, takes about 12.7 MB and needs a Dataset. When one round needs more than 6 MiB of rows, publish them once as a record Dataset in your project and name it at submission instead of sending records. A record Dataset split holds at most 10,000 rows and 64 MiB. Publishing needs datasets.write, which a Key from el auth login has when your project role is Developer or Admin. See Train from a Dataset.

  1. Write the rows as a record Dataset. --dataset adds r1/dataset/files/train/records.jsonl and r1/dataset/manifest.json:

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

    Each line keeps id, ability and language for tracing; only messages is training input. The manifest's source names the Hugging Face dataset and the full revision commit that v1.0.0 resolves to. A tag such as v1.0.0 is not accepted in source, so the script records the 40-character commit instead. EnvLoop keeps source as a record of origin and checks only the files you upload. The identity sits in the dataset object of the manifest:

    {
    "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. Validate, then publish. The dry run checks the manifest, file sizes and hashes, and your permission without creating anything:

    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

    Files larger than 8 MiB upload in resumable parts; if an upload stops, run the same command again with the same files. A published version is immutable. To change the rows, publish a new version.

  3. Submit. Remove records from the request, give it a new request_id, and name the Dataset and 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

    The platform reads the split at submission, checks every row like inline records, and freezes the content with the Job. The Job's dataset records the manifest_sha256 of the version, the sha256 of the split file and row_count. Monitoring, Checkpoints, deployment and evaluation are the same as above.

    In a reference run, one Dataset holding both rounds' 2,715 rows trained from the base model in 340 steps, in about two and a half hours. On the same 550 questions it scored 79.5%, against 25.8% for the base model.

Troubleshooting​

SymptomCause and fix
prepare_data.py stops with "over 6 MiB"The rows do not fit in one request. Train from a Dataset, or split the rows across rounds with parent_checkpoint.
A submission fails near 6 MiBThe request body is limited to 6 MiB, including the JSON around the records. Regenerate the request with the script rather than adding rows by hand.
The Job ends cancelled with BUDGET_EXCEEDED before a GPU startsThe cap is below limits.max_trial_cost_usd. Submit again with a new request_id and a higher cap. The same request_id with a different cap returns TRAIN_JOB_IDEMPOTENCY_CONFLICT.
Submitting the endpoint returns 402 INSUFFICIENT_BALANCEThe workspace balance does not cover one hour of the replicas. The message and details.required_usd give the amount (Submit an Inference Job). A workspace member with billing permission adds credit in the Console; then submit again with the same request_id. A refused submission creates no Job and holds no GPU.
el job metrics shows no rows for several minutesAllocation, startup and model loading come before the first step. Check status and phase with el job get, and the startup lines with el job log. PLANNED and LAUNCHING mean the Job is still waiting for its GPU.
The Job ends failed with EXECUTION_QUEUE_TIMEOUTThe Job waited longer than the queue limit before it could start. Nothing ran and no GPU was allocated, so nothing was charged. Submit again with a new request ID: el job submit --type train --spec r1/request.json --request-id NEW_ID --max-cost-usd YOUR_CAP_USD. prepare_data.py keeps the old request_id on purpose, so running it again returns the failed Job.
The Job stays running long after its last stepSaving the final Checkpoint and releasing the GPU take about a minute. Read the system log with el job log JOB_ID --source system --tail 50. If el checkpoint list --job-id JOB_ID still shows no Checkpoint 15 minutes after the last step, contact support with the Job ID.
cp_… or uri?Use uri for parent_checkpoint and adapter registration. Either works for el checkpoint download. Use cp_… for el checkpoint delete.
el setting cleanup or el setting operation wait returns 403The Key lacks the Resource cleanup permission. Check the release with el job get instead (Clean up), or use a Key from el auth login with a Developer or Admin role.
el setting cleanup returns 422 INVALID_CLEANUP_RESOURCESThe message names the entry and field. A Train Job entry is {"kind": "job", "id": "JOB_ID"}; the other kinds are endpoint, adapter and checkpoint.
Adapter registration returns 409 ADAPTER_BASE_MISMATCHThe profile's base model differs from the Checkpoint's. Use qwen35-4b@1 for Qwen/Qwen3.5-4B.
Inference returns 503 ENDPOINT_COLD_STARTINGThe worker is still loading. Wait for READY; evaluate.py retries after Retry-After.
Base and adapter score the sameCheck served in the result files. Without a suffix the Job calls the first adapter; use /base for the base model.
Long outputs and accuracy near zeroThinking was on. Send "chat_template_kwargs": {"enable_thinking": false}, as in training.
el data create returns 403 DATA_ACCESS_REQUIREDThe message names the missing permission, for example datasets.write. The Key came from el auth login --read-only, was issued without that permission, or your role is Viewer. A Key never gains permissions after it is issued, so retrying does not help. Run el auth login without --read-only (a Developer or Admin role is needed) and revoke the old Key; see Manage ANS data.
el data create returns FEATURE_NOT_INSTALLEDInstall envloop-cli[data] (Installation).
DATA_PUBLICATION_INVALIDThe manifest does not match the files, the identity is not inside dataset, or source.commit is a tag, branch or short hash. Rerun prepare_data.py instead of editing files by hand.
TRAINING_DATA_TOO_LARGE or TRAINING_DATA_UNAVAILABLEA split holds at most 10,000 rows and 64 MiB. The Dataset must be a published record Dataset of this project, with the named version and split.
Commands from an older guide failOld /v1/job/... routes return 404; use /v1/jobs/... and /v1/training-models. Options are spelled --timeout-seconds, not --timeout-sec. Check el COMMAND --help for the installed release.

Next: the recipe options in Training recipe parameters, the endpoint lifecycle in Deploy and call a trained model, and per-step metrics in Monitor Train Job.