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.

python
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")
Diagram

👤 You

📋 Training Form

🚀 POST /train/start

⚙️ Training Subprocess

📈 Progress Stream

✅ Checkpoint

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

  1. 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.

  2. Start the run

    The app posts your config to /train/start. Invalid configs are rejected immediately, before any model download begins.

  3. 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.

  4. Load the checkpoint

    When the run reports done, point an agent’s llm= 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.

MethodRequired dataset columns
sftany (falls back to the default dataset)
cpttext
dpoprompt, chosen, rejected
orpoprompt, chosen, rejected
cpoprompt, chosen, rejected
ktoprompt, completion, label
rewardchosen, rejected
grponeeds 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.

Diagram
⚙️ Trainer🧠 Engine📋 UI Form⚙️ Trainer🧠 Engine📋 UI FormPOST /train/start (config)preflight — reject bad config (400)spawn (own process group)log lines, loss, stepGET /train/progress?run=…&cursor=Nreplay from cursor, then followPOST /train/stop/{run_id}kill tree, release GPU

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.

RuleBehaviour
CaseIds are compared with casefold() — run-x collides with RUN-X
Same secondAuto-ids get a -2, -3… suffix instead of overwriting
Format1–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.

PlatformHow the trainer is killed
macOS / LinuxThe process group is signalled, killing the trainer and everything it spawned
Windowstaskkill /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.

CapValueWhat it bounds
MAX_HISTORY50Finished runs listed in history
MAX_METRICS5000Loss/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.

ResponseCause
400 Bad RequestMissing model_name or dataset, GRPO without reward_funcs, or a rejected run id
409 ConflictA run is already live — one GPU runs one job
500The 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.


Related