The expensive part of a small fine-tune is rarely the training. It's the GPU you rented for an afternoon, ran a 4-minute job on, and forgot to shut down.
This post runs a LoRA fine-tune of a small open model as a job: one command on a fresh GPU machine that stops itself, and its billing, the moment the command exits. The whole run below took 3 minutes 45 seconds of GPU time and cost $0.05. I ran it on 2026-10-21 and the output is copied from the logs.
What we're training
- Model:
Qwen/Qwen2.5-0.5B-Instruct, small enough to train in minutes - Data: 2,000 conversations from
trl-lib/Capybara, with 5% held out for evaluation - Method: LoRA (rank 16, all linear layers) with TRL's
SFTTrainer, 150 steps, bf16 - GPU: one RTX A6000 (48 GB)
It's deliberately tiny so you can run it end to end for pocket change. For a real model you change the model name, the dataset and the step count; nothing else in the workflow moves.
The script
Save this as finetune.sh. It sets up a clean Python environment, writes the training script and runs it:
set -e
pip install -q uv && uv venv -q /opt/env && . /opt/env/bin/activate
uv pip install -q torch trl peft datasets --torch-backend=cu128
cat > /workspace/train.py <<'EOF'
import torch
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
data = load_dataset("trl-lib/Capybara", split="train[:2000]").train_test_split(test_size=0.05, seed=0)
trainer = SFTTrainer(
model="Qwen/Qwen2.5-0.5B-Instruct",
train_dataset=data["train"],
eval_dataset=data["test"],
peft_config=LoraConfig(r=16, lora_alpha=32, target_modules="all-linear", task_type="CAUSAL_LM"),
args=SFTConfig(
output_dir="/workspace/qwen-capybara-lora",
max_steps=150,
per_device_train_batch_size=8,
learning_rate=2e-4,
bf16=True,
logging_steps=25,
eval_strategy="steps",
eval_steps=75,
save_steps=75,
report_to="none",
),
)
print("eval loss before:", round(trainer.evaluate()["eval_loss"], 4))
trainer.train()
print("eval loss after:", round(trainer.evaluate()["eval_loss"], 4))
trainer.save_model("/workspace/qwen-capybara-lora")
print("saved adapter to /workspace/qwen-capybara-lora")
EOF
python /workspace/train.py
Two lines in there cost me four failed runs, so here's why they're there:
- A fresh virtualenv. Installing TRL into the machine's system Python dragged in packages that clashed with what the image already had (an old Jinja2 broke chat templates). A uv venv sidesteps all of it and takes seconds.
-
--torch-backend=cu128. The machine's NVIDIA driver supports CUDA 12.8. A plainpip install torchpicked a newer CUDA build, which then refused the driver and fell back to CPU, and bf16 training failed. Match the PyTorch build to the driver (nvidia-smishows it).
Those failed runs cost between zero and three cents each, because each job stopped as soon as the command failed. That's the point of running this as a job.
Run it
Install the CLI and sign in once:
brew install aeonmindai/tap/runcrate
runcrate login
Then submit the job:
runcrate run --gpu A6000 --template ubuntu-train --max-runtime 40m --name qwen-lora -d -- "$(cat finetune.sh)"
0f122b1e-5e47-476c-9ed0-94990b8e7ccd
Job qwen-lora is starting. Follow it with: runcrate jobs logs -f 0f122b1e-5e47-476c-9ed0-94990b8e7ccd
-
--max-runtime 40mis the ceiling. If the training hangs or loops, the job is stopped at 40 minutes. The clock starts when your command starts, not while the machine boots. -
-dreturns straight away with the job id. Leave it off andruncrate runstreams the output until the job ends. -
runcrate runexits with the job's own exit code (124 if it hit the max runtime), so you can chain it in scripts or CI.
Read the logs
runcrate jobs logs 0f122b1e-5e47-476c-9ed0-94990b8e7ccd
The interesting lines:
eval loss before: 1.7612
{'loss': '1.53', ... 'mean_token_accuracy': '0.638', 'epoch': '0.2101'}
{'loss': '1.425', ... 'mean_token_accuracy': '0.6525', 'epoch': '0.3151'}
{'loss': '1.346', ... 'mean_token_accuracy': '0.6669', 'epoch': '0.4202'}
{'loss': '1.455', ... 'mean_token_accuracy': '0.6505', 'epoch': '0.5252'}
{'loss': '1.507', ... 'mean_token_accuracy': '0.6433', 'epoch': '0.6303'}
{'train_runtime': '165.3', 'train_samples_per_second': '7.26', 'train_steps_per_second': '0.908', 'train_loss': '1.46', 'epoch': '0.6303'}
eval loss after: 1.4237
saved adapter to /workspace/qwen-capybara-lora
Held-out loss went from 1.76 to 1.42 in 150 steps, which is what you'd expect from a first pass on chat data. Nothing to write home about as a model, but the loop works and it's the same loop at 7B.
What it cost
runcrate jobs status 0f122b1e-5e47-476c-9ed0-94990b8e7ccd
Status: ● succeeded
Exit code: 0
Runtime: 3m45s
Cost: $0.05 ($0.65/hr)
Created: 2026-10-21T01:08:23.404+00:00
Started: 2026-10-21T01:12:35.312+00:00
Ended: 2026-10-21T01:16:20.648+00:00
About four minutes between submit and start went to getting the machine ready. Then 3 minutes 45 seconds of command, and the machine was gone. $0.65 an hour was the A6000 rate when I ran this; prices move, so check the live pricing before you pick a GPU.
Keeping the adapter
In this run the adapter was saved to the machine's /workspace and went away with the machine, which is fine for a smoke test and useless for anything else. Two ways to keep it:
-
Attach a storage volume. With a volume attached,
/workspaceis the volume, and what you write there is saved back when the job ends, whatever its final status. The jobs docs cover it. -
Push to the Hugging Face Hub at the end of
train.pywithtrainer.push_to_hub(), passing your token as an environment variable on the job rather than inside the command (commands and logs are visible to everyone in the workspace).
Scaling it up
The same command takes bigger hardware: --gpu H100, or several GPUs on one machine with --gpu-count. A job can run up to 24 hours. Past that, or when you want to sit on the machine with Jupyter or VS Code, use an instance with a max runtime and idle shutdown instead.
Or ask your agent
The Runcrate MCP server exposes jobs to Claude Code, Codex and Cursor, so "fine-tune with ./finetune.sh on an A6000, cap it at 40 minutes, and tell me the final eval loss" is a prompt that works. Setup is one command per client on the MCP setup page.
Links
- Jobs: runcrate.ai/docs/jobs/overview
- CLI jobs reference: runcrate.ai/docs/cli/jobs
- TRL SFTTrainer: huggingface.co/docs/trl/sft_trainer
If you try this with your own data and it breaks somewhere I didn't hit, tell me where. Reply here or write to hello@runcrate.ai.
Written with AI assistance. Every command and output above was run on 2026-10-21.
Top comments (0)