Fine-Tuning
Train a model from the Desktop app and load the checkpoint into your agent
Fine-tune a model in the Desktop app, then point your agent at the checkpoint it produced.
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
llm="./runs/run-1/checkpoint", # the checkpoint your fine-tune produced
)
# Train in the Desktop app, then load the result like any other model.
agent.start("Answer in the style I trained you on")The app runs one training job at a time — two fine-tunes on one GPU do not co-exist — and the job survives closing the window, so you can reconnect and keep watching.
Quick Start
- Open the Fine-Tuning tab
Pick a method, a base model, and a dataset. The form collects everything the engine needs to start a run.
- Start the run
The app posts your config to
/train/start. Invalid configs are rejected immediately, before any model download begins. - Watch progress and stop when needed
Loss, step, and log lines stream live. Stop ends the run and releases the GPU. The checkpoint stays on disk under the run’s directory.
- Load the checkpoint
When the run reports
done, point an agent’sllm=at the checkpoint directory to chat with your fine-tuned model.
Supported Methods
The form supports these training methods. GRPO is validated but cannot be launched from the UI form — it needs reward_funcs, which the form does not collect, so run it from the command line.
| Method | Required dataset columns |
|---|---|
sft | any (falls back to the default dataset) |
cpt | text |
dpo | prompt, chosen, rejected |
orpo | prompt, chosen, rejected |
cpo | prompt, chosen, rejected |
kto | prompt, completion, label |
reward | chosen, rejected |
grpo | needs reward_funcs — CLI only |
How It Works
The engine writes your config, spawns a training subprocess in its own process group, and follows its output.
Run IDs
Each run becomes a directory, so run ids are unique case-insensitively — run-x and RUN-X are the same run because macOS and Windows filesystems fold case. Auto-generated ids append a numeric suffix, so two starts inside the same second no longer collide.
| Rule | Behaviour |
|---|---|
| Case | Ids are compared with casefold() — run-x collides with RUN-X |
| Same second | Auto-ids get a -2, -3… suffix instead of overwriting |
| Format | 1–64 characters of letters, digits, dot, dash, or underscore, starting alphanumeric |
Starting a run with an id that already exists returns 400 Bad Request.
Stop Semantics
The Stop button posts to /train/stop/{run_id}, naming the run to cancel — so a stale tab cannot cancel a newer run.
| Platform | How the trainer is killed |
|---|---|
| macOS / Linux | The process group is signalled, killing the trainer and everything it spawned |
| Windows | taskkill /T /F walks the child tree |
The GPU is released in every case. A run this engine did not spawn is only adopted when the recorded start-time fingerprint still matches the live pid. If the pid has been recycled to an unrelated process, the run is not adopted — it is already surfaced as terminal, so Stop has nothing to signal and no unrelated process group is ever SIGTERM’d from the trainer.
History & Retention
Finished runs stay in memory so the history pane can show them, within bounded caps. The full log file stays on disk.
| Cap | Value | What it bounds |
|---|---|---|
MAX_HISTORY | 50 | Finished runs listed in history |
MAX_METRICS | 5000 | Loss/metric points kept per run |
Preflight Validation
Invalid configs are rejected before a single byte is downloaded, so you learn about a mistake in seconds rather than after a multi-gigabyte model load.
| Response | Cause |
|---|---|
400 Bad Request | Missing model_name or dataset, GRPO without reward_funcs, or a rejected run id |
409 Conflict | A run is already live — one GPU runs one job |
500 | The runs directory could not be created |
Best Practices
Name your runs
An explicit run id makes the checkpoint directory predictable. Remember ids are case-insensitive — Exp-1 and exp-1 are the same run.
Run GRPO from the command line
GRPO needs reward_funcs, which the form cannot collect. Launch it with praisonai train instead and watch it from the same engine.
Keep the trainer in its own environment
praisonai-train pulls torch and unsloth, which you may keep in a separate CUDA-matched venv. Set PRAISONAI_TRAIN_CMD to choose the interpreter — --config <path> is always appended, so the override picks the interpreter, not the contract.
Reconnect, don't restart
Progress is a ring buffer replayed from a cursor and the run’s state is written to runs/<run-id>/run.json at every transition. Close the window and reopening the run replays what you missed. A run whose child process is still alive after an engine restart reappears as running and Stop still reaches it — provided the child’s start-time fingerprint still matches the pid we recorded. A recycled pid is refused, and the run appears as interrupted instead. State files written before this fingerprint was added (pre-upgrade) are also refused; at most one run shows as interrupted across a single upgrade restart.
Runs are adopted by identity, not just by pid
Adoption after a restart requires both the recorded pid and its start-time fingerprint to match the live process — a pid alone is not enough because the OS recycles pids. If it did not check, a recycled pid could be adopted as running (wedging Start behind a phantom run) and Stop could SIGTERM whatever unrelated program now holds that pid. The Rust shell guards its own engine the same way — see src-tauri/src/adopt.rs and its PidReused rejection.
Keep the log file for the full record
The chart shows only recent metrics. For the complete series, read train.log in the run directory.
Stop from the run's own tab
Stop names the run it cancels, so a stale tab is refused. Trigger Stop from the tab showing the run you actually want to end.