Autonomous LoRA fine-tuning workbench. Inspired by autoresearch — the same modify → train → measure → keep/discard loop, adapted from pretraining-from-scratch to LoRA fine-tuning with Unsloth.
To set up a new goal, work with the user to:
- Agree on a goal tag: propose a tag based on the objective (e.g.
sentiment-4b,summarizer). The branchautofinetune/<tag>must not already exist. - Create the branch:
git checkout -b autofinetune/<tag> main - Read the in-scope files: The repo is small. Read these files for full context:
program.md— these instructions (you're reading them now)prepare.py— fixed constants, data loading, utilities. Do not modify.train.py— fixed training harness. Do not modify.config.py— the only file you edit during experiments.
- Write
goal.md: Based on the user's objective, write agoal.mdthat defines:- Objective (what are we training for?)
- Primary metric (what number are we optimizing?)
- Data paths (where is training/eval data?)
- Evaluation criteria (how do we measure success?)
- Research directions (what knobs to try?)
- Constraints (memory, time, thinking mode on/off)
- Set up
config.py: Configure for this specific goal:- Model name, data paths, format
- Initial hyperparameters (reasonable defaults from goal.md)
- Custom
eval_custom_funcif the goal needs accuracy or other metrics beyond eval_loss
- Verify:
uv run prepare.py --verify— checks model name and data files exist - Initialize
results.tsv: Create with just the header row:commit eval_loss metric memory_gb status description - Confirm and go: Confirm setup looks good, then start the experiment loop.
Each experiment trains a LoRA adapter. The training script runs for a fixed time budget (default 5 minutes, configurable via TIME_BUDGET env var). Launch it as: uv run train.py > run.log 2>&1
What you CAN do:
- Modify
config.py— this is the only file you edit. Everything is fair game: model choice, LoRA rank, learning rate, batch size, epochs, data formatting, custom eval functions, replay buffers, target modules, etc.
What you CANNOT do:
- Modify
train.pyorprepare.py. They are read-only infrastructure. - Install new packages or add dependencies.
- Modify the training harness or evaluation logic (use
eval_custom_funcin config.py for custom metrics).
The goal is defined in goal.md. Read it to know what metric you're optimizing. Usually eval_loss (lower is better) or a custom metric like eval_accuracy (higher is better).
The first run: Always establish a baseline first — run with the initial config.py unchanged.
The training script prints a summary like this:
---
eval_loss: 1.2345
eval_accuracy: 0.8750
training_seconds: 300.1
total_seconds: 320.5
peak_memory_gb: 12.3
adapter_size_mb: 45.2
num_epochs: 3
Extract key metrics:
grep "^eval_loss:\|^eval_accuracy:" run.logWhen an experiment is done, log it to results.tsv (tab-separated, NOT comma-separated).
The TSV has a header row and 6 columns:
commit eval_loss metric memory_gb status description
- git commit hash (short, 7 chars)
- eval_loss achieved (e.g. 1.2345) — use 0.0000 for crashes
- primary metric from goal.md if applicable (e.g. eval_accuracy) — use 0.0000 if N/A or crash
- peak memory in GB, round to .1f — use 0.0 for crashes
- status:
keep,discard, orcrash - short text description of what this experiment tried
Example:
commit eval_loss metric memory_gb status description
a1b2c3d 1.2345 0.8750 12.3 keep baseline
b2c3d4e 1.1890 0.9100 12.5 keep increase LR to 5e-4
c3d4e5f 1.3200 0.8200 12.3 discard LoRA rank 8
d4e5f6g 0.0000 0.0000 0.0 crash batch size 32 (OOM)
The experiment runs on a dedicated branch (e.g. autofinetune/sentiment-4b).
LOOP FOREVER:
- Read
results.tsv,config.py, andgoal.mdto understand current state - Decide what to try next (informed by goal.md research directions and past results)
- Edit
config.pywith your experimental change git commit config.py -m "experiment: [description]"- Run:
uv run train.py > run.log 2>&1(redirect everything — do NOT let output flood your context) - Extract results:
grep "^eval_loss:\|^eval_accuracy:\|^peak_memory_gb:" run.log - If grep output is empty → crash. Run
tail -n 50 run.logto read the stack trace - Log to
results.tsv(do NOT commit results.tsv — keep it untracked) - If primary metric improved → keep (advance the branch, keep the commit)
- If worse or no improvement → discard:
git reset --hard HEAD~1and delete the adapter
These are the dimensions you can explore. The order and priority is up to you — decide based on results, intuition, and what goal.md suggests:
- Learning rate — the full range from 1e-5 to 5e-4 (and beyond)
- LoRA rank — 8, 16, 32, 64. Higher rank = more capacity but more memory
- Number of epochs — 1-5+. More can help or overfit
- Data formatting — system prompt variations, input structure
- Sequence length — profile your data, match to actual distribution
- Batch size / gradient accumulation — effective batch size = per_device * grad_accum
- LoRA target modules — add or remove projection layers
- Scheduler — cosine, linear, constant, warmup variations
- Custom eval function — measure what actually matters for the goal
- Replay buffer — mix general data to prevent forgetting
- LoRA alpha — scaling factor, experiment with alpha:rank ratio
- Weight decay, dropout — regularization knobs
Use results.tsv to guide your strategy. If something works, explore nearby. If stuck, try something radically different.
- Typo/import error: Fix config.py, recommit, retry.
- OOM: Log as crash. Decide how to address the memory constraint.
- Training divergence (loss goes to inf/nan): Log as crash. Investigate what caused it.
- Broken idea: Log as crash, discard, try something else.
- Can't fix after 2-3 attempts: Skip the idea entirely. Log crash, move on.
Once the experiment loop has begun (after initial setup), do NOT pause to ask the human if you should continue. Do NOT ask "should I keep going?" or "is this a good stopping point?". The human might be asleep or away and expects you to continue working indefinitely until manually interrupted. You are autonomous.
If you run out of ideas:
- Re-read goal.md for research directions you haven't tried
- Try combining successful changes
- Try more radical changes (different model size, very different LR)
- Try the opposite of what worked (sometimes reveals insights)
- Read the research docs in
docs/research/for domain-specific tips
The loop runs until the human interrupts you, period.
- Thinking mode: Disable for classification tasks (add
/no_thinkor useenable_thinking=False). Enable for reasoning tasks. - Precision: Always bf16. Never use QLoRA/4-bit quantization.
- Chat template: Uses
<|im_start|>/<|im_end|>format. SFTTrainer handles this automatically with chat-format datasets. - Model names: Use
unsloth/Qwen3-{size}for optimized versions (e.g.unsloth/Qwen3-4B,unsloth/Qwen3-0.6B). - Target modules:
q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_projcovers all projection layers.
Since config.py is Python (not YAML), you can:
-
Define custom eval functions above CONFIG:
def eval_custom(model, tokenizer, eval_dataset_path): # ... run inference, compute accuracy ... return {"eval_accuracy": accuracy} CONFIG["eval_custom_func"] = eval_custom
-
Define custom formatting functions:
def my_formatter(example): return f"Task: {example['task']}\nAnswer: {example['answer']}" CONFIG["formatting_func"] = my_formatter
-
Use conditional logic:
import os CONFIG["per_device_train_batch_size"] = 8 if os.environ.get("BIG_GPU") else 4