PraisonAI Train

Fine-tune a base model with Unsloth or improve an agent iteratively with LLM/human feedback — one command each

Fine-tune a base model with Unsloth or improve an agent iteratively with LLM/human feedback — one command each.

Diagram

Agent Training

📋 Scenario

🤖 praisonai-train agents

⭐ Grader / Human

✅ Improved Agent

LLM Fine-tuning

📋 Dataset

🤖 praisonai-train llm

🧠 LoRA Model

✅ Hugging Face / Ollama

Prefer a UI? See Desktop → Training

The PraisonAI Desktop app wraps praisonai-train llm in a Train tab — live loss chart, log, and reconnect-safe progress.

Standalone Install

Training now ships as its own package (praisonai-train, import praisonai_train). Install just what you need:

bash
pip install praisonai-train              # agent training (LLM-as-Judge + human feedback), no CUDA/Unsloth
pip install "praisonai-train[llm]"       # + Unsloth / torch fine-tuning stack

The standalone CLI exposes the training commands directly:

bash
praisonai-train agents --input "What is Python?" --iterations 3
praisonai-train llm dataset.json --model llama-3.1
praisonai-train list
praisonai-train show <session>
praisonai-train apply <session>

Quick Start

  1. LLM Fine-tuning

    Fine-tune a base model on your dataset with Unsloth. Pick the tab for your model family — the trainer uses each model’s own chat template, so chat_template is optional.

    bash
    pip install "praisonai-train[llm]"

    Llama

    bash
    praisonai-train llm dataset.json \
        --model unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit

    Optional YAML override: chat_template: "llama-3.1".

    Gemma

    bash
    praisonai-train llm dataset.json \
        --model unsloth/gemma-2-2b-it-bnb-4bit

    Optional YAML override: chat_template: "gemma" (or "gemma-2").

    Qwen

    bash
    praisonai-train llm dataset.json \
        --model unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit

    Optional YAML override: chat_template: "qwen-2.5".

  2. Agent Training

    Improve an agent iteratively with LLM-as-Judge or human feedback.

    python
    from praisonaiagents import Agent
    
    agent = Agent(instructions="You are a helpful assistant.")
    bash
    pip install praisonai-train
    
    praisonai-train agents --input "What is Python?"

    One pip install praisonai-train gives you the agent, the trainer, and the grader — litellm ships as a core dependency, so live LLM-as-Judge grading works with no extra install.

    Runs cleanly on Windows and other non-UTF-8 consoles — the summary automatically falls back to ASCII (PASSED / NEEDS WORK) with no configuration needed. Exit codes: 0 = training persisted, 1 = training failed, 130 = interrupted. See Train CLI → Exit Codes.


Beginner-safe defaults

A minimal config trains locally and never pushes to a hub unless you ask it to.

A config with just model_name, max_seq_length, and dataset: trains locally and saves the LoRA to lora_model/. Publishing to Hugging Face or Ollama is opt-in — set the flag and the target name. Response-only masking auto-probes the tokenizer, so assistant_only_loss: auto is safe on any template.

Diagram

Just train locally

Publish to Hugging Face

Publish to Ollama

default

ollama_push: false

What do I want?

No publish flags
saves to lora_model/

huggingface_save: true
+ hf_model_name

ollama_save: true
+ ollama_model

Upload?

Pushed to ollama.com

Local Ollama model only

Three safety guarantees:

  • Minimal config trains locally — no push, no missing-repo crash.
  • assistant_only_loss: auto never crashes on a stock Gemma / Qwen / Llama template — it falls back to full-sequence SFT.
  • Unknown / misnamed keys warn instead of crashing — typos are collected into one block with did you mean suggestions and training continues.

Three runtime NOTEs surface behaviour that used to be silent — read them if the console shows something unexpected:

  • num_samples slices the head, before shuffle. Setting dataset[].num_samples: N without shuffle: true trains on the first N rows — often sorted by topic and unrepresentative. The trainer prints NOTE: num_samples takes the FIRST N rows before shuffle; add shuffle: true to sample randomly. Add shuffle: true to the dataset entry to sample uniformly.
  • split_type: validation/test/eval is added to training, not held out. These splits are concatenated into the training set. The trainer warns: WARNING: dataset split_type 'validation' is ADDED to the TRAINING data (not held out). Use val_split_ratio for a held-out eval set. Use val_split_ratio: 0.05 for a real held-out set.
  • max_steps: 2800 fires when neither length is set. With neither num_train_epochs nor max_steps, the trainer defaults to max_steps: 2800 and prints NOTE: neither num_train_epochs nor max_steps set; defaulting to max_steps: 2800. Set num_train_epochs: 1-3 (or max_steps) to control training length.

Preflight validation

Before any model download or GPU load, the trainer fails fast with one friendly line for the five most common non-developer misconfigurations.

Diagram

Missing / too old

OK

No CUDA GPU

Publishing on, no HF token

< 10 GB free on training disk

quantization_method typo

All checks pass

praisonai-train llm config.yaml

Training deps
current?

❌ deps missing or too old

Preflight

❌ No CUDA GPU detected

❌ No Hugging Face credentials

❌ Low disk

❌ quantization_method ‘q4km’ is not valid

✅ Model load starts

The version-floor check runs first — it lives inside the lazy import helper, before the trainer reaches the CUDA probe.

Preflight checkMessage when it failsFix
Training dependency versionsTraining dependencies are missing or too old. + Missing: <names> / <name> <ver> is older than the required <floor> + Install with: pip install -U "<name>>=<floor>" ...Run the printed pip install -U ... command — it upgrades all six at once.
CUDA GPU (skipped when train: false)No CUDA GPU detected. Fine-tuning needs a GPU. On a rented box check the driver; on CPU it cannot run.Use a GPU box; on a rented VM re-install CUDA drivers. Set train: false to publish/export on CPU without training.
Hugging Face credentials (only when huggingface_save / huggingface_save_gguf / push_to_hub is on)Publishing to Hugging Face is enabled but no credentials were found. Run 'huggingface-cli login' or 'export HF_TOKEN=hf_...'. To train without publishing, set huggingface_save: false.Run huggingface-cli login (cached tokens count) or export HF_TOKEN=hf_....
Low disk (< 10 GB free on output_dir or .)Low disk: only 3.2 GB free on the training disk (need ~10 GB minimum). Free up space or set output_dir to a larger disk.Free space, or set output_dir in config.yaml to a larger volume.
Quantization method (only when huggingface_save_gguf or ollama_save is on)quantization_method 'q4km' is not valid. Choose one of: ...Use one of the 35 values below.

The 35 valid quantization_method values — 24 standard plus 11 imatrix — are listed in full under Quantization method. The imatrix (IQ) family is what lets a large model fit on a laptop.


How It Works

Point the trainer at a base model and dataset to fine-tune, or at a scenario to iteratively improve an agent.

Diagram
Grader / HF / OllamaTrainerUserGrader / HF / OllamaTrainerUserpraisonai-train llm dataset.jsonFine-tune with LoRA (Unsloth)Push model with HF_TOKENPublished modelpraisonai-train agents –input “…”Grade agent output (LLM or human)Score + suggestionsRe-run with feedback (up to N iterations)Improved agent + session ID

Real interaction — agent-training path: an agent gives so-so answers → run praisonai-train agents --input "Explain AI" --human → review N iterations → praisonai-train apply {session_id} --run "Explain AI" bakes the improvements into the agent via hooks → the same agent answers better.


CLI Reference

The praisonai-train CLI exposes fourteen subcommands, grouped into data (generate, validate, benchmark, dedup, from-trials), training (llm, agents), serving / export / inference (serve, export, checkpoints, infer), and utilities (list, show, apply).

SubcommandWhat it doesDeep link
generateProduce synthetic instruction/response rows from a recipeDataset Tooling
validateScore / QC an existing JSONL against the recipe’s checksDataset Tooling → validate
benchmarkCompare deployments by throughput, latency, and success rateSpeed Benchmark
dedupExact + MinHash/LSH near-dedup across many JSONL filesCross-file dedup
data from-trialsExport passing agent trials to a trainer-ready SFT dataset (+ provenance)Dataset Tooling → from-trials
llmFine-tune a base LLM on a generated datasetTrain CLI → llm
agentsDistil an agent workflow into a dataset for trainingTrain CLI → agents
serveServe a fine-tuned GGUF over an OpenAI-compatible endpoint (auto-fetches the MTP drafter on Gemma-4)Serve & MTP Fast-Inference
exportPublish an already-trained model (hf / gguf / ollama) without re-running trainingTrain CLI → export
checkpointsList what a run saved — after training, before you export or resumeList Checkpoints
inferOne-shot prompt against a model you just trained — a “did it work?” checkInfer
listList locally available recipes / adaptersTrain CLI → list
showShow a recipe’s schema and defaultsTrain CLI → show
applyApply a trained adapter / merge with the base weightsTrain CLI → apply

See Train CLI for full flags.


Serve a trained model

Run a trained GGUF on an OpenAI-compatible endpoint — MTP fast inference auto-enables for Gemma-4.

bash
# Serve a trained Gemma-4 model on http://localhost:8080/v1 (MTP auto-enabled)
praisonai-train serve --gguf model.gguf

# Or point at a trained model directory (GGUF auto-detected)
praisonai-train serve -d lora_model

# One-shot speed test with drafter acceptance rate
praisonai-train serve --gguf model.gguf --benchmark

# Custom port, no MTP
praisonai-train serve --gguf model.gguf --no-mtp-draft --port 9000

Every flag, with its default:

FlagTypeDefaultDescription
--ggufstrunsetPath to the target GGUF to serve.
--model-dir / -dstrunsetDirectory of the trained model (GGUF auto-detected, drafter files skipped).
--config / -cstrunsetOptional config.yaml for base-model / MTP knobs.
--base-modelstrunsetBase model id used to select the MTP drafter.
--mtp-draft / --no-mtp-draftboolTrueUse the stock MTP drafter — on automatically when the family supports it.
--spec-draft-n-maxint2Max tokens the MTP drafter proposes per step.
--portint8080Port for llama-server.
--nglint99Number of layers to offload to the GPU.
--benchmarkboolFalseRun a one-shot speed benchmark instead of serving.

A non Gemma-4 model serves without MTP and says so — no configuration needed. The base model name (which drives drafter selection) is recovered in this order: --base-model > --config model_name > the model dir’s config.json _name_or_path.


Export an already-trained model

Publish or convert a locally-trained model without re-running training.

bash
# Publish a locally-trained model to Ollama with a q4_k_m quantization
praisonai-train export ollama --model-dir lora_model --ollama yourname/mymodel --quant q4_k_m

# Publish merged weights to Hugging Face
praisonai-train export hf --model-dir lora_model --hf yourname/mymodel-hf

# Produce a LOCAL .gguf (ready to serve) and also push it to the Hub
praisonai-train export gguf --model-dir lora_model --quant q4_k_m --hf yourname/mymodel-gguf

The first argument is the target — ollama, gguf, or hf. Every flag:

FlagTypeDefaultDescription
--model-dir / -dstrrequiredDirectory of the already-trained model.
--config / -cstrunsetOptional config.yaml for extra export knobs.
--ollamastrunsetOllama model name, e.g. yourname/mymodel. Required for the ollama target.
--hfstrunsetHugging Face repo id, e.g. yourname/mymodel. Required for the hf target.
--quantstrunsetQuantization method for gguf / ollama, e.g. q4_k_m.
--base-modelstrunsetBase model id (for chat-template selection).
--mtp-draft / --no-mtp-draftboolFalseAlso download the stock MTP drafter (Gemma-4 only) for fast inference.

The gguf target always writes a local .gguf (so you can serve it) and only additionally pushes to the Hub when --hf is set. The model name (which drives chat-template selection) falls back to the trained model’s config.json _name_or_path, then the directory name, when --base-model / model_name aren’t given.


Clean errors & preflight

Fine-tuning validates the environment up front and fails with a single actionable line — no traceback — so a non-developer knows exactly what to fix.

Diagram

No

Yes

No

Yes

Missing

OK

No

Yes

No

Yes

🚀 praisonai-train llm

Training deps current?

ERROR: deps missing or too old

CUDA GPU?

ERROR: No CUDA GPU detected

Publishing + HF creds?

ERROR: no HF credentials

≥ 10 GB free?

ERROR: Low disk

quantization_method valid?

ERROR: invalid quantization_method

✅ Training runs

What to expect:

  • Training deps missing or too old → a single Training dependencies are missing or too old. block naming each package plus one pip install -U ... command, raised before any heavy import.
  • No CUDA GPU / missing HF credentials / low disk (< 10 GB) / bad quantization_method → a single ERROR: {reason} line and exit 1, checked before any download starts.
  • Ctrl-C → prints Interrupted. and exits 130.
  • Unknown config keys → one grouped WARNING: block with a did you mean '{key}'? suggestion for each; training continues.

GPU out of memory

If the GPU runs out of memory during training, the run stops and prints an ordered fix.

Diagram

💥 GPU out of memory

Step 1

max_seq_length: 2048

Step 2

use_gradient_checkpointing: unsloth

Step 3

halve batch size, double grad accumulation

Step 4

switch to a 4-bit base model

✅ Run fits in memory

The CLI attaches the fix to the ERROR: line on stderr:

text
The GPU ran out of memory. In order of what usually helps most:
  1. Lower max_seq_length (try 2048, or 4096 if you were higher)
  2. Set use_gradient_checkpointing: unsloth (if it is off)
  3. Lower per_device_train_batch_size, raising gradient_accumulation_steps
     by the same factor to keep the effective batch size
  4. Use a smaller model, or a 4-bit one (load_in_4bit: true)

Apply the steps to config.yaml, cheapest first — re-run after each and stop once the run fits.

  1. Lower max_seq_length

    Sequence length is the biggest single lever. Drop it to 2048 (try 4096 only if you were already higher).

    yaml
    max_seq_length: 2048
  2. Enable gradient checkpointing

    Trades a little speed for a large memory saving.

    yaml
    use_gradient_checkpointing: unsloth
  3. Halve the batch, double accumulation

    Keep the effective batch size while halving memory — halve per_device_train_batch_size and double gradient_accumulation_steps.

    yaml
    per_device_train_batch_size: 1
    gradient_accumulation_steps: 8
  4. Switch to a 4-bit base model

    Last resort — pick a 4-bit base or load the current one in 4-bit.

    yaml
    model_name: unsloth/llama-3.1-8b-bnb-4bit
    load_in_4bit: true

Fine-tuning Setup

Push a fine-tuned model to Hugging Face and Ollama.

Hugging Face token

A Hugging Face token does two jobs: downloading a gated base (Llama, Gemma) and publishing a fine-tuned model. Set it once via the env var or config hf_token.

bash
export HF_TOKEN="${HF_TOKEN:?Set HF_TOKEN in your shell}"

Train a gated Llama base — pass the token by either channel:

yaml
# config.yaml
model_name: "meta-llama/Meta-Llama-3.1-8B-Instruct"
hf_token: "hf_xxx"   # or export HF_TOKEN — same token, either channel

Precedence: the HF_TOKEN env var wins, then a cached huggingface-cli login, then config hf_token. See Model access & reproducibility and Hub Privacy.

Model access & reproducibility

Reach gated and custom-code models, and pin a revision so a multi-day run is reproducible.

KeyTypeDefaultDescription
hf_tokenstr—Config-level HF token. Downloads gated bases and authenticates publishing. Overridden by HF_TOKEN.
trust_remote_codebool—Required for custom-code models. Only sent when set.
revisionstr—Pin a HF branch, tag, or commit SHA. Only sent when set.
yaml
# config.yaml
model_name: "meta-llama/Meta-Llama-3.1-8B-Instruct"
revision: "main"            # reproducible across days
trust_remote_code: false

See Unsloth Surface → Model access.

Save merged locally

Merge the adapter into a folder on this disk — no Hub account, no token, no network.

yaml
# config.yaml
merged_save_dir: "./merged-model"
save_method: "merged_16bit"   # default; also "merged_4bit_forced", "lora"

The merge step runs on every run and is a no-op unless merged_save_dir is set. See Unsloth Surface → Offline merged export.

Fast rollouts for GRPO

grpo previously always ran the slow HF-generate path for its rollouts. Flip fast_inference: true to swap it for vLLM.

yaml
# config.yaml
method: grpo
fast_inference: true
gpu_memory_utilization: 0.6   # only forwarded when fast_inference is on
max_lora_rank: 32             # only forwarded when fast_inference is on

See Unsloth Surface → vLLM fast rollouts and Reward Functions & GRPO.

Conda setup: what the script installs on your machine

setup_conda_env.sh probes the host for a GPU before creating the env, so a non-Nvidia box gets a truthful build instead of a working-looking CUDA one.

Diagram

nvidia-smi OR /proc/driver/nvidia/gpus

rocminfo OR amd-smi

Linux, none of the above

setup_conda_env.sh

Detect accelerator

cuda

rocm

cpu

pytorch=2.6.0 pytorch-cuda=12.4
-c pytorch -c nvidia

pytorch=2.6.0 CPU build
+ WARNING → pytorch.org

pytorch=2.6.0 CPU build
+ WARNING: llm refuses CPU

The probe order is nvidia-smi → /proc/driver/nvidia/gpus → rocminfo / amd-smi → CPU. The pytorch-cuda=12.4 pin now lives in exactly one place, chosen by the detected accelerator, so the two conda create branches cannot disagree.

An AMD host installs the CPU build and prints — the installer does not build a ROCm stack for you, so follow the official PyTorch instructions then re-run:

text
WARNING: an AMD GPU was detected. This installer only knows the CUDA
         and CPU builds, so it will install the CPU one. Follow
         https://pytorch.org/get-started/locally/ for a ROCm build,
         then re-run this script.

A CPU-only Linux host installs the CPU build and prints — the env is usable for dataset work and exports, but not fine-tuning (see GPU check):

text
WARNING: no GPU detected. Fine-tuning needs one -- praisonai-train
         llm refuses to run on CPU. The environment will still be
         usable for dataset work and exports.

macOS still gets a CPU build with no warning (unchanged).

Chat Template

Leave chat_template unset for tokenizers that ship their own — the trainer uses the model’s native template and only overrides it when you set the key.

Diagram

Gemma 2 / Gemma 3

Qwen 2.5 / Qwen3

Llama 3.x

No built-in template

Model?

Leave chat_template unset
native Gemma template

Leave unset OR
chat_template: qwen-2.5

Leave unset OR
chat_template: llama-3.1

Must set chat_template
or trainer raises ValueError

If a model has no built-in template and you leave chat_template unset, the trainer fails fast with a clear error you can grep for:

text
Tokenizer for model '<name>' has no chat template and none was provided via config 'chat_template'. Set 'chat_template' (e.g. 'gemma', 'qwen-2.5', 'llama-3.1') so conversations format correctly.

Training method (method:)

method: picks the trainer — sft (default) for instruction tuning, cpt for continued pretraining on a raw-text corpus, or a preference method.

method:Trainer usedRequired dataset columnsNotes
sft (default)SFTTrainerChat (conversations or messages) or Alpaca (formatted to text)Standard supervised fine-tuning. messages is the shape praisonaiagents.eval.export_sft writes.
cptUnslothTrainertextContinued pretraining — raw-text corpus. See Continued Pretraining.
dpoDPOTrainerprompt, chosen, rejectedPreference.
orpoORPOTrainerprompt, chosen, rejectedPreference.
ktoKTOTrainerprompt, completion, labelPreference.
grpoGRPOTrainerpromptScored by reward functions; supports fast vLLM rollouts. See Reward Functions & GRPO.

Multi-Model Fine-tuning

Gemma and Qwen now fine-tune out of the box with their native templates — no config needed.

bash
praisonai-train llm dataset.json --model unsloth/gemma-2-2b-it-bnb-4bit
praisonai-train llm dataset.json --model unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit

Both commands are the two models the SDK actually tests (2× RTX A6000). Forcing llama-3.1 on every model was the old default and silently corrupted Gemma/Qwen training.

Assistant-only Loss

Response-only loss trains on assistant replies only — prompts are masked out of the loss. It’s on by default via assistant_only_loss: auto.

yaml
assistant_only_loss: auto   # default — mask whenever any route is available

The trainer masks through two routes, chosen automatically:

  1. assistant_only_loss (TRL’s built-in) — used when the chat template ships {% generation %} markers.
  2. train_on_responses_only (Unsloth’s turn-marker masking) — the fallback that masks by locating literal turn-marker strings. It works on Llama, Qwen, Gemma, Mistral, and Phi templates — none of which ship {% generation %} today.
Diagram

false

true / auto

Yes

No

Yes

No

assistant_only_loss

Masking wanted?

Train on full sequence

Template has
generation blocks?

Route: assistant_only_loss
TRL built-in

Turn markers
known for template?

Route: train_on_responses_only
Unsloth turn markers

ValueError — no route available

auto resolves to mask whenever any route is available — the template supports {% generation %}, or turn markers are known for it.

ValueBehaviourWhen to use
autoMask whenever any route is available, else full-sequence SFTRecommended default — masks correctly on every listed template
trueForce masking; fails fast at model-prep time if no route is availableYou want masking guaranteed
falseForce off — train on the full sequenceYou want full-sequence loss regardless

train_on_responses_only is accepted as an alias for assistant_only_loss if you’re migrating from an older config. Both real YAML booleans (assistant_only_loss: true) and the string forms ("true" / "false" / "yes" / "no" / "on" / "off") work.

The run-summary Loss mask: line names which route actually ran:

RouteLine printed
offLoss mask: full sequence (training on prompts too)
assistant_only_lossLoss mask: assistant replies only (TRL assistant_only_loss)
train_on_responses_onlyLoss mask: assistant replies only (unsloth turn markers)

Each Unsloth chat template maps to a marker family used by the turn-marker route:

Chat templateFamilyInstruction markerResponse marker
llama-3, llama3, llama-3.1, llama-31, llamallama-3`<\start_header_id\>user<\end_header_id\>\n\n``<\start_header_id\>assistant<\end_header_id\>\n\n`
chatml, qwen-2.5, qwen3, phi-4, gpt-oss, yi-chat, unsloth, gemma_chatml, …chatml`<\im_start\>user\n``<\im_start\>assistant\n`
gemma, gemma2, gemma-3, gemma3, gemma-3n, gemma-4gemma<start_of_turn>user\n<start_of_turn>model\n
mistralmistral[INST][/INST]
phi-3, phi-35, phi-3.5phi`<\user\>\n``<\assistant\>\n`
zephyrzephyr`<\user\>\n``<\assistant\>\n`

Marker resolution matches longest-first (so llama-3.1 beats llama), is case-insensitive, and normalises hyphens/dots (gemma-2 == gemma2). When chat_template is unset the resolver falls back to the model’s repo name only (not the unsloth/ org prefix). An unknown model resolves to no markers — the trainer refuses to guess rather than mask on the wrong boundary and train on nothing.

Initilise praisonai train

bash
praisonai train init

Requirements

Install training dependencies:

bash
pip install "praisonai-train[llm]"       # LLM fine-tuning (Unsloth / torch / CUDA)
pip install praisonai-train              # agent training (LLM-as-Judge + human feedback) — no CUDA/Unsloth
pip install "praisonai[train]"           # via the wrapper — pulls praisonai-train[llm]

Pick the flavour that matches your flow: agent training (praisonai-train) needs no CUDA/Unsloth — its base install already pulls litellm for live LLM-as-Judge grading — while LLM fine-tuning (praisonai-train[llm]) pulls the full torch/Unsloth stack.

pip install "praisonai-train[llm]" installs the whole ML stack for you — you only run the base install:

PackageMinimum floor
transformers>= 4.51.3
unsloth>= 2025.9.1
trl>= 0.18.2
peft>= 0.13.0
accelerate>= 0.34.0
bitsandbytes>= 0.45.0
torch>= 2.6.0

The CUDA-12.4 pin is imposed by the conda installer, not required by torch — a torch 2.6 wheel built for another CUDA (or ROCm) also satisfies the runtime check. Verified end-to-end on unsloth 2026.7.4 / trl 0.24.0 / torch 2.10 / peft 0.19.1, across Qwen2.5-0.5B and Gemma-2-2B.

Runtime version-floor enforcement

The six floors are checked at runtime, not just by pip — because pip is routinely bypassed.

Diagram

Missing / too old

All six OK

praisonai-train llm

Version floors
satisfied?

❌ one line + pip install -U …

✅ heavy imports begin

pip install --no-deps, a hand-built conda env, and older setup_conda_env.sh torch pins all install a stack pip never validated. The trainer re-checks the six floors itself, before importing torch/transformers/unsloth/etc.

PackageFloor
torch2.6.0
transformers4.51.3
unsloth2025.9.1
trl0.18.2
peft0.13.0
bitsandbytes0.45.0

A matching release with a dev or rc suffix satisfies the floor — 2.6.0rc1 counts as 2.6.0, so testing an upstream prerelease is not blocked.

A missing or stale stack prints one block naming exactly what to fix:

text
Training dependencies are missing or too old.
Missing: unsloth
torch 2.5.1 is older than the required 2.6.0
Install with: pip install -U "unsloth>=2025.9.1" "torch>=2.6.0"

Run the pip install -U "<name>>=<floor>" ... command the trainer printed — it upgrades every listed package in one go.

Required for training:

  1. Huggingface token
  2. Base model to train on (e.g. unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit)
  3. Dataset to train on (e.g. yahma/alpaca-cleaned)
  4. Huggingface model name to upload to (e.g. mervinpraison/llama3.1-instruct) (Optional)
  5. Ollama model name to upload to (e.g. mervinpraison/llama3.1-instruct) (Optional)

If training dependencies are missing when you run praisonai-train llm, you’ll see one of two messages depending on what’s installed.

When praisonai-code is not installed (bare pip install praisonai-train):

LLM fine-tuning dependencies not installed
Install with: pip install "praisonai-train[llm]"

When praisonai-code is installed but a downstream import failed (e.g. torch/unsloth missing):

Failed to load LLM fine-tuning runner: <underlying ImportError>

The wrapper’s older pip install "praisonai[train]" hint is no longer printed from the praisonai-train llm entrypoint (PraisonAI PR #3053).

Diagram
sequenceDiagram
    participant User
    participant CLI as praisonai-train llm
    participant Bridge as _code_bridge

    User->>CLI: praisonai-train llm dataset.json
    CLI->>Bridge: import praisonai_code.cli.main
    alt praisonai_code absent
        Bridge-->>CLI: ImportError + code_available()=False
        CLI-->>User: "LLM fine-tuning dependencies not installed"<br/>Install with: pip install "praisonai-train[llm]"
    else praisonai_code present but downstream import fails
        Bridge-->>CLI: ImportError + code_available()=True
        CLI-->>User: "Failed to load LLM fine-tuning runner: &lt;real error&gt;"
    else all deps present
        Bridge-->>CLI: PraisonAI runner
        CLI-->>User: Fine-tune runs
    end

Supported model families

The trainer uses each base model’s own chat template by default — no more force-applied Llama template — so Gemma, Qwen, and Llama all format correctly out of the box.

Model familyExample base modelchat_template value
Llama 3.xunsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"llama-3.1" (or leave unset — model has its own)
Gemma 2 / Gemma 3unsloth/gemma-2-2b-it-bnb-4bit"gemma" (or leave unset)
Qwen 2.5 / Qwen 3unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit"qwen-2.5" (or leave unset)
Mistral / Phivia native templateleave unset

To upload to ollama.com (Linux)

bash
sudo cat /usr/share/ollama/.ollama/id_ed25519.pub

Save the output to ollama.com → Ollama keys.

Ollama template selection

The trainer picks an Ollama TEMPLATE and stop-token block per model family, matched case-insensitively against model_name.

Family keywordTurn markers / templatingStop tokens
llama`<start_header_id> / <eot_id>` header blocks`<start_header_id>, <end_header_id>, <eot_id>`
qwen`<im_start> / <im_end> (adds num_ctx 32768` + Qwen system prompt)`<endoftext>`
mistral[INST] ... [/INST][INST], [/INST]
phi`<im_start>…<im_sep>``<im_start>, <im_end>, <im_sep>`
deepseekbare role content—
llava`<im_start> / <im_end>`</s>, USER:, ASSSISTANT:
gemma{start_of_turn} / {end_of_turn}, system folded into first user turn{end_of_turn}, {start_of_turn}
anything elsefalls back to a Llama-style header template`<start_header_id>, <end_header_id>, <eot_id>`

See the full templates in the trainer source (prepare_modelfile_content).

Ollama environment variables

Point the trainer at a configured or remote daemon and a bigger models disk.

VariablePurposeDefaultWhen to set
OLLAMA_HOSTAddress the trainer probes and where ollama create/push publish to. Accepts host, host:port, or http://host:port (IPv6 literal [::1]:11434 supported).127.0.0.1:11434A configured or remote Ollama daemon on a non-default host — stops the trainer ignoring a healthy remote daemon.
OLLAMA_MODELSDirectory Ollama stores models in. The trainer pre-checks free space on this volume before ollama create.~/.ollama/modelsThe default volume is small — point at a large disk with export OLLAMA_MODELS=/big/disk/ollama-models.

Disk pre-check

Before ollama create runs, the trainer estimates the space it needs at ~1.5× the source model size and refuses to start if the OLLAMA_MODELS volume has less free:

Not enough disk for the Ollama model: 4.2 GB free on '/home/me/.ollama/models', need ~15.0 GB. Point Ollama at a bigger disk, e.g. `export OLLAMA_MODELS=/big/disk/ollama-models`.

Quantization passthrough

quantization_method in the config flows through to ollama create --quantize {q}, so models aren’t stored as huge f16. It’s the same key GGUF export uses, and it’s validated against the 35 valid values up front.

Daemon startup + surfaced errors

If the trainer starts ollama serve and the HTTP API on /api/version doesn’t respond within 15 seconds, the raised RuntimeError includes the ollama serve stderr — so port in use or a bad OLLAMA_HOST shows up in the error instead of vanishing.

Push failures & key registration

A push whose stderr matches unauthorized / 401 / 403 / forbidden / invalid key translates to a key-registration hint, with your public key inlined when it’s readable so you can paste it straight into ollama.com:

Ollama rejected the push (unauthorized). Register your public key at https://ollama.com/settings/keys and make sure the model is namespaced as `<username>/<name>`.

Your public key (~/.ollama/id_ed25519.pub):
ssh-ed25519 AAAAC3Nz...

A non-zero exit from ollama create raises a RuntimeError with the captured stderr (e.g. ` ollama create me/model:latest failed: Error: invalid Modelfile ) instead of a raw CalledProcessError` traceback.


Config.yaml example

Drive an LLM fine-tuning run from a config file instead of flags.

Minimal — train locally

No publish flags — trains and saves the LoRA to lora_model/, pushing nowhere.

yaml
method: sft                # sft (default) | dpo | orpo | kto — see Preference Training
model_name: "unsloth/gemma-2-2b-it-bnb-4bit"
max_seq_length: 2048
dataset:
  - name: "yahma/alpaca-cleaned"
    num_samples: 20        # subset for a fast smoke test (honored as of #3279)

Publish to HF + Ollama

Publishing defaults off — set the flag and the matching target name. Real YAML booleans (not "true" strings).

yaml
model_name: "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"
max_seq_length: 2048
dataset:
  - name: "yahma/alpaca-cleaned"

huggingface_save: true              # real YAML bool; "true" string also works
hf_model_name: "yourname/llama-3.1-instruct-lora"

ollama_save: true
ollama_model: "yourname/llama3.1-instruct"

assistant_only_loss: auto           # tri-state — auto probes the template (default)

Multi-GPU + checkpoints + eval

A realistic combined run: two GPUs, checkpoint every 50 steps, keep the 3 best, hold out 5% for eval, and stop early if it plateaus. See Newer keys (from PraisonAI #3283) for every key below.

yaml
model_name: "unsloth/gemma-2-2b-it-bnb-4bit"
max_seq_length: 2048
dataset:
  - name: "yahma/alpaca-cleaned"

# Checkpoint every 50 steps, keep only the 3 most recent
save_strategy: "steps"
save_steps: 50
save_total_limit: 3
final_model_dir: "lora_model"
resume_from_checkpoint: true   # picks up the latest if a checkpoint exists

# Hold out 5% for eval; keep the best checkpoint; stop if flat for 3 evals
val_split_ratio: 0.05
load_best_model_at_end: true
metric_for_best_model: "eval_loss"
early_stopping_patience: 3
bash
# 2 GPUs on one machine
torchrun --nproc_per_node=2 -m praisonai_train.train.llm.trainer train --config config.yaml

Both real YAML booleans (train: true) and the string forms (train: "true") work — pick whichever reads better. This holds for vision configs too, as of PraisonAI #4879; before then a real YAML boolean crashed the vision path on .lower().

The full-config reference below shows every optional key with its default.

yaml
# Training method — sft (default) | dpo | orpo | kto. Case-insensitive.
method: sft

model_name: "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"
model_parameters: "8b"

# Preference-tuning knobs (dpo / orpo / kto only) — commented out for SFT.
# beta: 0.1                 # preference-loss temperature (dpo/orpo/kto)
# max_prompt_length: 1024   # prompt truncation (default: max_seq_length // 2)
# desirable_weight: 1.0     # kto only — weight on thumbs-up examples
# undesirable_weight: 1.0   # kto only — weight on thumbs-down examples

# Publishing — all default OFF. Set the flag AND its target to push.
huggingface_save: true
hf_model_name: "yourname/llama-3.1-instruct"
ollama_save: true
ollama_model: "yourname/llama3.1-instruct"

# Optional: force a specific Unsloth chat template (default: model's own).
# Required when the base model's tokenizer has no built-in chat template.
chat_template: "llama-3.1"   # or "gemma", "qwen-2.5"

# Response-only loss — tri-state, default "auto" (probe the template).
assistant_only_loss: auto

dataset:
  - name: "yahma/alpaca-cleaned"
    split_type: "train"
    processing_func: "format_prompts"
    rename:
      input: "input"
      output: "output"
      instruction: "instruction"
    filter_data: false
    filter_column_value: "id"
    filter_value: "alpaca"
    num_samples: 20000   # Now honored — trains on the first 20000 rows of the split (PraisonAI #3279).

dataset_text_field: "text"
dataset_num_proc: 1        # optional — default is unset (Unsloth sizes workers from memory)
packing: false

max_seq_length: 2048
load_in_4bit: true
lora_r: 16
lora_target_modules:
  - "q_proj"
  - "k_proj"
  - "v_proj"
  - "o_proj"
  - "gate_proj"
  - "up_proj"
  - "down_proj"
lora_alpha: 16
lora_dropout: 0
lora_bias: "none"
use_gradient_checkpointing: "unsloth"
random_state: 3407
use_rslora: false
loftq_config: null

# Optional: pick the right chat template for the model family.
# Omit to use the model's own built-in template.
chat_template: "llama-3.1"      # or "gemma", "gemma-2", "qwen-2.5", "mistral", "phi-3", ...

# Optional: mask prompts out of the loss (auto picks a masking route; see Assistant-only Loss).
assistant_only_loss: auto

per_device_train_batch_size: 2
gradient_accumulation_steps: 2
warmup_steps: 5
num_train_epochs: 1
max_steps: 10
learning_rate: 2.0e-4
logging_steps: 1
optim: "adamw_8bit"
weight_decay: 0.01
lr_scheduler_type: "linear"
seed: 3407
output_dir: "outputs"

quantization_method:
  - "q4_k_m"
bash
praisonai-train llm dataset.json

Silent-behaviour notes

The trainer prints these at runtime — they change results without erroring, so know them up front.

New / newly-honored fine-tuning config keys

These keys map straight to the trainer in praisonai_train/train/llm/trainer.py — all optional, all backward-compatible.

KeyTypeDefaultDescription
methodstr"sft"Training method — sft, dpo, orpo, or kto. Case-insensitive. An unknown value raises ValueError listing every supported method. See Preference Training.
modelstr—Alias for model_name. Accepted for parity with the --model CLI flag; if both are set, model_name wins.
chat_template`str \null`null (use model’s own)Override the tokenizer’s built-in chat template. Common values: "gemma", "qwen-2.5", "llama-3.1". Trainer raises ValueError if the model has no template and this is unset.
assistant_only_loss`”auto” \bool`"auto"Mask prompts out of the loss. "auto" masks whenever a route is available (TRL {% generation %} or Unsloth turn markers); true forces on (errors only when neither route is available); false forces off. train_on_responses_only is accepted as an alias. SFT only — ignored for preference methods.
betafloattrainer defaultdpo / orpo / kto. Preference-loss temperature, passed straight to the trainer’s beta.
max_prompt_lengthintmax_seq_length // 2dpo / orpo / kto. Prompt truncation length for preference methods.
desirable_weightfloattrainer defaultkto only. Weight on thumbs-up (label: true) examples.
undesirable_weightfloattrainer defaultkto only. Weight on thumbs-down (label: false) examples.
trainbooltrueSet false to skip training and publishing (validation still runs).
huggingface_saveboolfalsePush merged LoRA to Hugging Face. Requires hf_model_name.
huggingface_save_ggufboolfalsePush GGUF quantizations to Hugging Face. Requires hf_model_name.
ollama_saveboolfalsePush to Ollama. Requires ollama_model.
ollama_pushbooltruePush to ollama.com after ollama create. Set false to keep the model local only.
quantization_method`str \list[str]`—GGUF/Ollama quantization. A single-element list (e.g. ["q4_k_m"], as shipped by older templates) is unwrapped to the scalar automatically.
dataset[].num_samplesint—Train on the first N rows of the split. Must be a positive integer — validated on load. Now honored as of #3279 (was silently ignored before).
lora_rint16LoRA rank. Now honored from config (was hardcoded pre-#3274).
lora_alphaint16LoRA alpha. Now honored from config.
lora_target_moduleslist[str]["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"]LoRA target modules. Now honored from config.
lora_dropoutfloat0LoRA dropout. Now honored from config.
lora_biasstr"none"LoRA bias mode. Now honored from config.
use_gradient_checkpointing`str \bool`"unsloth"Gradient checkpointing mode. Now honored from config.
random_stateint3407PEFT seed. Now honored from config.
use_rsloraboolfalseEnable RS-LoRA. Now honored from config.
loftq_config`dict \null`nullLoftQ config. Now honored from config.
dataset_num_proc`int \null`nullDataset tokenization workers (goes on SFTConfig). When unset, Unsloth sizes workers from available memory. Set to 1 for deterministic, single-process tokenization; set a larger integer to pin an explicit count.
packingboolfalseEnable sequence packing (goes on SFTConfig).
num_train_epochs`int \null`nullIf set and max_steps unset, train by epochs instead of steps.

Hub privacy & preference-tuning keys (from PraisonAI #4357)

PraisonAI #4357 made every Hub push private by default and added preference tuning (dpo / orpo / kto). All keys are optional.

KeyTypeDefaultDescription
hf_privatebooltruePush as a private repo. Set false to publish publicly. Accepts "false" / "0" / "no". See Hub Privacy.
save_methodstr"merged_16bit"Passed to Unsloth’s push_to_hub_merged. Was hardcoded before #4357.
commit_messagestr—Optional Hub commit message. Only sent when set.
tagslist[str]—Optional Hub repo tags. Only sent when set.
methodstr"sft"Training method: sft, cpt, dpo, orpo, kto, grpo (case-insensitive). See Preference Tuning and Reward Functions & GRPO.
betafloatTRL defaultPreference strength (β). dpo / orpo / kto only.
max_prompt_lengthintmax_seq_length // 2Max prompt tokens for preference methods.
desirable_weightfloat1.0KTO weight for thumbs-up examples.
undesirable_weightfloat1.0KTO weight for thumbs-down examples.

Newer keys (from PraisonAI #3283)

PraisonAI #3283 added multi-GPU, checkpointing, eval, early stopping, and a raw-SFTConfig escape hatch — all optional, all backward-compatible.

Checkpointing & resume — see Checkpointing:

KeyTypeDefaultDescription
save_strategy`”no” \“steps” \“epoch”`"steps" if save_steps set, else "no"When to write checkpoints.
save_stepsint100Step interval when save_strategy: steps.
save_total_limitintunsetKeep only the N most recent checkpoints.
save_safetensorsbooltrueSerialise in safetensors format (dropped with a warning if the installed TRL doesn’t accept it).
resume_from_checkpoint`bool \str`falsetrue = latest in output_dir; a path = that checkpoint.
final_model_dirstr"lora_model"Where the final adapter is saved (and reloaded for inference).

Evaluation & best-checkpoint:

KeyTypeDefaultDescription
val_split_ratiofloatunsetFraction held out as eval (e.g. 0.05). Required for load_best_model_at_end / early_stopping_patience.
eval_strategy`”no” \“steps” \“epoch”`"steps" if eval_steps set, else "epoch"When to evaluate.
eval_stepsint100Step interval when eval_strategy: steps.
per_device_eval_batch_sizeintinherits per_device_train_batch_size (or 2)Batch size for eval.
load_best_model_at_endboolfalseReload the best checkpoint at end. Auto-aligns save/eval strategies.
metric_for_best_modelstr"eval_loss"Metric to rank checkpoints on.
greater_is_betterboolfalseDirection of the ranking metric.

Early stopping:

KeyTypeDefaultDescription
early_stopping_patienceintunsetStop after N evals with no improvement. Requires val_split_ratio. Auto-enables load_best_model_at_end.
early_stopping_thresholdfloat0.0Minimum change to count as improvement.

Multi-GPU (DDP) — see Multi-GPU:

KeyTypeDefaultDescription
ddp_find_unused_parametersbooltrue (under DDP)Keep true for multimodal / MoE; set false for a small speedup on dense-text models.

Quantization / precision:

KeyTypeDefaultDescription
dtype`”float16″ \“bfloat16” \null`nullTorch dtype for the loaded model (Unsloth decides if unset).
load_in_8bitboolunset8-bit quantisation. Mutually exclusive with load_in_4bit.
full_finetuningboolfalseTrain the base weights directly, skip LoRA entirely.

Advanced LoRA:

KeyTypeDefaultDescription
modules_to_savelist[str]unsetExtra modules to fully train (e.g. ["embed_tokens", "lm_head"]). Required for method: cpt to actually adapt the vocabulary. Without it, CPT trains no embeddings and prints a NOTE.
embedding_learning_ratefloatlearning_rate / 10Only used when method: cpt. Rate for embed_tokens / lm_head; the default is the recipe UnslothTrainer was written to enforce. See Continued Pretraining.
rank_patterndictunsetPer-module LoRA ranks (PEFT).
alpha_patterndictunsetPer-module LoRA alphas (PEFT).
use_doraboolfalseDoRA — Weight-Decomposed Low-Rank Adaptation.
finetune_last_n_layersintunsetTune only the last N transformer blocks — the cheap-run recipe.
layers_to_transform`int \list[int]`unsetExplicit layer indices to adapt.
layers_patternstrunsetRegex / pattern selecting which layers to adapt.
target_parameterslist[str]unsetParameter selectors — required to LoRA MoE experts.
init_lora_weights`str \bool`unsetLoRA init scheme, e.g. "gaussian", "pissa", "olora".

See Unsloth Surface → PEFT selectors for the “which recipe?” decision guide and MoE examples.

Extra optimization / logging / hub knobs:

KeyTypeDefaultDescription
max_grad_normfloatunsetGradient clipping norm.
warmup_ratiofloatunsetAlternative to warmup_steps (setting it pops warmup_steps).
neftune_noise_alphafloatunsetNEFTune noise scale.
dataloader_num_workersintunsetTorch DataLoader workers.
data_seedintunsetDeterministic dataset shuffle.
adam_beta1 / adam_beta2 / adam_epsilonfloatunsetAdamW hyperparameters.
group_by_lengthboolunsetLength-aware batching.
lr_scheduler_kwargsdictunsetPassthrough to the LR scheduler.
logging_first_stepbooltrueLog at step 0.
report_tostr"wandb" if PRAISON_WANDB else "none"none / tensorboard / wandb.
run_namestrinherits PRAISON_WANDB_RUN_NAME or "praisonai-train"Wandb run name.
push_to_hubboolfalsePush checkpoints to the Hub during training.
hub_model_idstrinherits hf_model_nameRepo id for push_to_hub.
hub_strategystr"every_save"Hub push cadence.

Generic training_arguments: passthrough — set any raw SFTConfig field. The trainer merges it into the config it hands TRL, then drops any field the installed TRL’s SFTConfig doesn’t accept with a WARNING (so version drift warns instead of crashing).

yaml
training_arguments:
  torch_compile: true
  gradient_checkpointing_kwargs:
    use_reentrant: false

Multi-GPU

Launch across every GPU with one torchrun command — the trainer detects the distributed launch and handles DDP.

bash
torchrun --nproc_per_node=2 -m praisonai_train.train.llm.trainer train --config config.yaml
Diagram

🚀 torchrun

⚙️ rank 0

⚙️ rank N

🧠 per-rank model load

🧠 per-rank model load

✅ rank-0 save + publish

Each rank loads the full model on its own GPU (device_map = {"": local_rank}), Unsloth runs through its DDP-safe checkpointing path, and only rank 0 writes the final adapter and publishes. Set ddp_find_unused_parameters: false for a small speedup on dense-text models (leave the default true for multimodal / MoE). See Multi-GPU for the full walkthrough.


Checkpointing & Resume

Save mid-run so an interruption never costs the whole training — then resume from the latest.

KeyTypeDefaultDescription
save_strategy`”no” \“steps” \“epoch”`"steps" if save_steps set, else "no"When to write checkpoints.
save_stepsint100Step interval when save_strategy: steps.
save_total_limitintunsetKeep only the N most recent checkpoints.
resume_from_checkpoint`bool \str`falsetrue = latest in output_dir; a path = that checkpoint.
final_model_dirstr"lora_model"Where the final adapter is saved.

Resume after an interruption — re-launch the same command with:

yaml
resume_from_checkpoint: true   # picks the latest checkpoint in output_dir

See Checkpointing for the full reference.


Eval + Early Stopping

Hold out an eval set to monitor overfitting, keep the best checkpoint, and stop when it plateaus.

Diagram

Yes

No

Yes

No

Keep the best model?

val_split_ratio
+ load_best_model_at_end

Train to max_steps

Stop when it plateaus?

Unsupported markdown: list

Keep best at end

val_split_ratio carves the eval set; load_best_model_at_end reloads the best checkpoint; early_stopping_patience stops after N flat evals (and requires val_split_ratio). See Checkpointing → Evaluation & Best-Checkpoint.


Config validation

The trainer runs a preflight pass on load — before any model, dataset, GPU, or download is touched — so a non-developer gets a clean, actionable error in seconds instead of a traceback after minutes.

Diagram

missing / too old

ok

missing

ok

missing

ok

yes

no

yes

no

yes

no

yes

no

📋 config.yaml

Training deps current?

❌ pip install -U …

Required keys?

❌ minimal example

Publish target set?

❌ hf_model_name / ollama_model required

train: true
+ no GPU?

❌ No CUDA GPU

Publishing
+ no HF creds?

❌ Run huggingface-cli login

< 10 GB free?

❌ Low disk

Bad –quant?

❌ invalid quantization

✅ Train / publish

Required keys

Preflight checks

CheckTriggerFailure message (paraphrased)
Training dependency versionspraisonai-train llm invoked with a missing/stale torch / transformers / unsloth / trl / peft / bitsandbytesTraining dependencies are missing or too old. + Missing: <names> / <name> <ver> is older than the required <floor> + Install with: pip install -U "<name>>=<floor>" ...
Required keysmodel_name, max_seq_length, or dataset missingConfig is missing required keys: [...]. Minimal example: ...
Publish target sethuggingface_save / huggingface_save_gguf true but hf_model_name missinghf_model_name is required when huggingface_save or huggingface_save_gguf is enabled.
Publish target setollama_save: true but ollama_model missingollama_model is required when ollama_save is enabled.
CUDA available (only when train: true, default)No CUDA GPUNo CUDA GPU detected. Fine-tuning needs a GPU. On a rented box check the driver; on CPU it cannot run.
HF credentialsAny publish flag set but neither HF_TOKEN nor a cached huggingface-cli loginPublishing to Hugging Face is enabled but no credentials were found. Run \huggingface-cli login\ or \export HF_TOKEN=hf_…\. To train without publishing, set huggingface_save: false.
Free diskoutput_dir (or .) has < 10 GB freeLow disk: only X.X GB free on the training disk (need ~10 GB minimum). Free up space or set output_dir to a larger disk.
Quantization methodhuggingface_save_gguf or ollama_save true and quantization_method outside the allowed setquantization_method '{val}' is not valid. Choose one of: ... — the full 35-value list is under Quantization method.

The required-keys message shows a copy-paste minimal config:

ValueError: Config is missing required keys: ['model_name', 'max_seq_length', 'dataset']. Minimal example:
  model_name: unsloth/gemma-2-2b-it-bnb-4bit
  max_seq_length: 2048
  dataset:
    - name: yahma/alpaca-cleaned

GPU check

Enforced only when train: true (the default). A publish-only or export-only run (train: false) is valid on CPU — cross-link Publishing.

No CUDA GPU detected. Fine-tuning needs a GPU. On a rented box check the driver; on CPU it cannot run.

Version floors

Six packages are version-checked at runtime, before any heavy import — pip install --no-deps and hand-built conda envs routinely bypass pip’s metadata.

PackageFloor
torch2.6.0
transformers4.51.3
unsloth2025.9.1
trl0.18.2
peft0.13.0
bitsandbytes0.45.0

Run the pip install -U "<name>>=<floor>" ... line the trainer prints — it upgrades every stale package at once. See Runtime version-floor enforcement for the full behaviour.

Hugging Face credentials

When publishing is enabled, either an HF_TOKEN / HUGGING_FACE_HUB_TOKEN env var or a cached huggingface-cli login (read via huggingface_hub.get_token()) is accepted — you don’t need both.

Publishing to Hugging Face is enabled but no credentials were found. Run `huggingface-cli login` or `export HF_TOKEN=hf_...`. To train without publishing, set huggingface_save: false.

Disk headroom

The trainer needs at least 10 GB free on the output_dir volume (or . when output_dir is unset).

Low disk: only 3.2 GB free on the training disk (need ~10 GB minimum). Free up space or set output_dir to a larger disk.

Quantization method

Enforced only when a GGUF or Ollama push is enabled. 35 values are valid — 24 standard plus 11 importance-matrix (IQ) quants. An unknown value lists all valid choices. The IQ family is what lets a large model fit on a laptop.

Standard (24):

bf16  f16  f32  fast_quantized  not_quantized  quantized
q2_k  q2_k_l  q3_k_l  q3_k_m  q3_k_s  q3_k_xs
q4_0  q4_1  q4_k  q4_k_m  q4_k_s
q5_0  q5_1  q5_k  q5_k_m  q5_k_s
q6_k  q8_0

IMatrix — large-model / laptop-fit (11):

iq1_m  iq1_s  iq2_m  iq2_s  iq2_xs  iq2_xxs
iq3_m  iq3_s  iq3_xxs  iq4_nl  iq4_xs

Supply your own importance matrix with imatrix_file, or leave it unset and Unsloth auto-fetches one from unsloth/<base>-GGUF. See Unsloth Surface.

Unknown keys

Unrecognized keys don’t crash — the trainer collects all of them and prints one block with the closest known key suggested via difflib:

WARNING: unrecognized config key(s) — these will be ignored:
  - num_sample  (did you mean 'num_samples'?)
  - max_step    (did you mean 'max_steps'?)

Training continues after the warning.

Unknown model name

A model_name outside the unsloth catalog also warns rather than refuses — unsloth’s loader falls through to a generic path for models it doesn’t map, so a typo shouldn’t block a working config. On a close match:

WARNING: 'unsloth/Meta-Llama-3.1-8B-Instrct-bnb-4bit' is not in unsloth's model list, so it may not load. Did you mean: unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit? Run `praisonai-train models` to see the full list.

With no close match, it lists three known-good starting points instead. Suggestions match the repo name (not the full id), so a right model under the wrong org surfaces the unsloth/… mirror. Run praisonai train models to see the whole list. Training continues after the warning.

See Export a trained model to publish an already-trained model without re-running training.


Publishing

Publishing to Hugging Face or Ollama is opt-in — set the flag and the matching target.

Diagram

Just training locally

Publish to HF

Publish to Ollama

default

ollama_push: false

Where to?

No flags
saves to lora_model/

huggingface_save: true
+ hf_model_name

ollama_save: true
+ ollama_model

Upload?

Pushed to ollama.com

Local Ollama model only

train: false is a valid config — the trainer skips training and publishing (previously it crashed with an AttributeError on a None model).

Publish without re-training

Already trained a model? Publish it with praisonai train export — no dataset, no re-training.

bash
praisonai-train export hf     --model-dir lora_model --hf     me/my-model
praisonai-train export gguf   --model-dir lora_model --hf     me/my-model --quant q4_k_m
praisonai-train export ollama --model-dir lora_model --ollama me/my-model --quant q4_k_m

for_export() skips the training-only validation — an export-only run needs no dataset: (or a config file at all). See Export an already-trained model.

Publishing failure modes

Actionable messages replace raw HTTP tracebacks.

Ollama publish knobs

Ollama exports quantize on create and can be redirected to a bigger disk.

  • --quantize — export ollama --quant q4_k_m passes --quantize to ollama create so the model isn’t stored as a huge f16.
  • OLLAMA_MODELS — set this env var to point Ollama at a larger disk when short on space. The trainer disk-pre-checks it before ollama create and fails with export OLLAMA_MODELS=/big/disk/ollama-models guidance instead of a cryptic “no space left on device”.
  • Push 401 / 403 → an actionable message: register your public key (~/.ollama/id_ed25519.pub) at ollama.com/settings/keys and namespace the model as {username}/{name}. The key value is printed for you to copy.

Hugging Face push errors

A rejected Hub push turns into a one-line fix, not a stack trace.

SituationMessage you’ll seeFix
HF token invalid / expiredHugging Face rejected the token (401). It is invalid or expired — run \huggingface-cli login\ or \export HF_TOKEN=hf_…\ with a valid write token.New write-scoped token
HF wrong repo owner or read-only tokenNo write access to '{repo}' (403). The repo must be under your own username and the token must have write scope. Set hf_model_name to '{your-username}/{name}'.Namespace to your user; write token
Ollama key not registeredOllama rejected the push (unauthorized). Register your public key at https://ollama.com/settings/keys and make sure the model is namespaced as '{username}/{name}'. plus your key printed inline from ~/.ollama/id_ed25519.pubPaste the printed key at the URL
Ollama disk fullNot enough disk for the Ollama model: X GB free on '{path}', need ~Y GB. Point Ollama at a bigger disk, e.g. \export OLLAMA_MODELS=/big/disk/ollama-models\.Move $OLLAMA_MODELS
Ollama daemon didn’t startollama serve did not become ready in 15 seconds. ollama serve output: {stderr}Fix the underlying stderr (port busy, bad OLLAMA_HOST)
ollama create failedRaised as a RuntimeError with the create-command stderr, not a raw CalledProcessErrorFollow the printed guidance

Run summary

Just before training starts, the trainer prints a one-block summary so you can confirm the config resolved as intended.

──────────── PraisonAI Train ────────────
  Model:       unsloth/gemma-2-2b-it-bnb-4bit
  Examples:    950  (+50 eval)
  Loss mask:   assistant replies only
  Steps:       200  ·  batch 2 × accum 2 × 2 GPUs
  Checkpoints: steps @ 50 steps  ·  Output: lora_model/
─────────────────────────────────────────

The Loss mask: line is the resolved auto / true / false decision — assistant replies only or full sequence. The block now also reports the GPU count (× N GPUs under torchrun), the held-out eval count (+N eval), the checkpoint strategy, and the final final_model_dir — so you can verify the whole run resolved as intended without reading the DEBUG noise.


Non-developer errors

The trainer translates the failures hit in practice into a single clean ERROR: line — never a traceback for these.

What went wrongError you see
No GPU on a training runERROR: No CUDA GPU detected. Fine-tuning needs a GPU. On a rented box check the driver; on CPU it cannot run.
Publishing enabled, no HF loginERROR: Publishing to Hugging Face is enabled but no credentials were found. Run huggingface-cli login or export HF_TOKEN=hf_...
< 10 GB free diskERROR: Low disk: only X.X GB free on the training disk (need ~10 GB minimum). Free up space or set output_dir to a larger disk.
Wrong --quant valueERROR: quantization_method 'q4km' is not valid. Choose one of: ... — the full 35-value list is under Quantization method.
HF token rejectedERROR: Hugging Face rejected the token (401). It is invalid or expired — run huggingface-cli login or export HF_TOKEN=hf_... with a valid write token.
No write access to HF repoERROR: No write access to '{repo}' (403). The repo must be under your own username and the token must have write scope. Set hf_model_name to '{your-username}/{name}'.
Ollama daemon didn’t come upERROR: ollama serve did not become ready in 15.0 seconds. + the captured ollama serve stderr
Ollama key not registeredERROR: Ollama rejected the push (unauthorized). Register your public key at https://ollama.com/settings/keys ... + your local ~/.ollama/id_ed25519.pub
Not enough disk on the Ollama volumeERROR: Not enough disk for the Ollama model: X.X GB free on '{dir}', need ~Y GB. Point Ollama at a bigger disk, e.g. export OLLAMA_MODELS=/big/disk/ollama-models

Everything else — a genuine bug — still shows its full traceback so it’s not silently swallowed. Ctrl-C exits 130.


Silent-behaviour NOTES

Three behaviours that used to surprise users now print an explicit NOTE: / WARNING: line.

  • num_samples takes the first N rows before shuffle — repeated runs with the same seed use the same deterministic subset.
  • split_type of validation / valid / test / eval is ADDED to the training set — no validation set is held out. Warned via WARNING: dataset split_type '{x}' is ADDED to the training set.
  • Default max_steps: 2800 fires when neither num_train_epochs nor max_steps is set: NOTE: neither num_train_epochs nor max_steps set; defaulting to max_steps: 2800. Set num_train_epochs: 1-3 (or max_steps) to be explicit. When both are set, max_steps silently wins (logged at DEBUG).
  • dataset.processing_func is ignored — advertised in old templates but never read. Warned via WARNING: dataset.processing_func (...) is not supported and will be ignored; formatting is chosen automatically from the dataset's columns.
  • quantization_method: ["q4_k_m"] (single-element list) is unwrapped to the scalar — older templates shipped it as a one-element list; both forms now work.

Advanced

Fine-tune loss to assistant turns only, in a model-agnostic way.

Model & template keys

Two optional keys control how conversations are formatted and which tokens count toward the loss.

OptionTypeDefaultDescription
chat_templatestrNoneUnsloth chat-template name (e.g. "llama-3.1", "gemma", "gemma-2", "qwen-2.5", "qwen-3", "mistral", "phi-3", "chatml"). Omit to keep the model’s own built-in template. See Unsloth’s get_chat_template for the full list.
assistant_only_loss`”auto” \bool`"auto"Mask prompts out of the loss. Masks via TRL’s {% generation %} route when the template has it, else Unsloth’s turn-marker route for any known template family (Llama / Qwen / Gemma / Mistral / Phi). See Assistant-only Loss.

Model-agnostic response-only training

Leave assistant_only_loss: auto to let the trainer mask the loss to assistant turns whenever any route is available. Because none of Unsloth’s chat templates ship {% generation %} today, the marker-based route (train_on_responses_only) is what actually masks on Gemma / Qwen / Llama / Mistral / Phi — no template change needed.

yaml
model_name: "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"
chat_template: "llama-3.1"
assistant_only_loss: true   # force on — errors only if NO route is available

The trainer picks the route automatically: TRL’s assistant_only_loss when the template exposes {% generation %} blocks, otherwise Unsloth’s train_on_responses_only turn markers for any known template family. You only hit an error when neither route exists — an unknown template with no known markers.

Which chat_template should I use?

Diagram

Yes, and it’s correct

No, or overriding

Llama

Gemma

Qwen

Mistral

Phi

Yes

No

📋 Fine-tuning a model

Does the tokenizer
ship its own template?

✅ Omit chat_template
use the built-in

Model family?

⚙️ llama-3.1 / llama-3

⚙️ gemma / gemma-2

⚙️ qwen-2.5 / qwen-3

⚙️ mistral

⚙️ phi-3

Need assistant_only_loss?

✅ auto masks via unsloth
turn markers — no template change

✅ Ready to train

Under the hood — modern TRL SFT surface

The trainer targets the current TRL API. Anyone writing custom training code should note it now uses SFTConfig in place of TrainingArguments, passes processing_class= instead of the removed tokenizer= kwarg, moves dataset_text_field, max_length (TRL renamed max_seq_length → max_length), packing, and dataset_num_proc onto SFTConfig, and lets modern TRL tokenize the text column internally instead of pre-tokenizing with a dummy field.


Weights & Biases

Track loss curves and checkpoints for each run.

bash
wandb login
bash
export PRAISON_WANDB=True
export WANDB_LOG_MODEL=checkpoint
export WANDB_PROJECT=praisonai-test
export PRAISON_WANDB_RUN_NAME=praisonai-train

Best Practices

Start with praisonai-train alone if you only want agent training

pip install praisonai-train pulls praisonaiagents plus litellm (for LLM-as-Judge grading) — enough for agents, list, show, and apply. Add [llm] only when you need Unsloth fine-tuning.

Set HF_TOKEN in the environment

Export HF_TOKEN in your shell before fine-tuning so the trainer can push to Hugging Face. Never commit the raw token.

Start small, then scale

Use a low max_steps and num_samples: 20 for a first run — a full smoke test finishes in under a minute. Ramp up once the pipeline is green.

Leave assistant_only_loss: auto

The trainer enables masking whenever the tokenizer supports it and falls back cleanly otherwise. Only set true / false if you need to force a specific behaviour.

Publish flags are opt-in

For local experiments, delete the huggingface_save / ollama_save keys entirely — no target, no push, no crash. Set the flag and its target (hf_model_name / ollama_model) only when you want to publish.

Publish flags are opt-in for vision runs too (PraisonAI #4879)

Before #4879 the vision trainer defaulted every publish flag to the string "true", so a plain vision config that never mentioned huggingface_save pushed the fine-tuned model to the Hub — or crashed with KeyError: 'hf_model_name' after training had already completed. It now behaves like the LLM path: publishing runs only when the flag and its target (hf_model_name for huggingface_save / huggingface_save_gguf, ollama_model for ollama_save) are both set. YAML booleans (train: true, huggingface_save: true) work in vision configs too — the old code crashed calling .lower() on a real bool.

Publishing needs credentials + write access — check them once

Run huggingface-cli login (or export HF_TOKEN=hf_...) and namespace the repo under your own username. Preflight catches missing credentials before the run; a rejected push still translates 401/403 into an actionable fix. For Ollama, register your signing key at ollama.com — a rejected push prints your public key to copy.

Track runs with Weights & Biases

Set PRAISON_WANDB=True and the WANDB_* variables to log loss curves and checkpoints for each run.

Let the tokenizer's native template win

Only set chat_template for models without one, or to force a specific one. Forcing llama-3.1 on Gemma/Qwen was the old default and silently corrupted training — now the trainer uses each model’s own template by default.

Match chat_template to your model family

Omit chat_template to use the model’s built-in template — that’s correct for most Instruct models. Set it explicitly (gemma, qwen-2.5, llama-3.1, …) only when overriding or when the tokenizer ships no template, otherwise the trainer raises ValueError at model prep.

Enable assistant_only_loss for instruction datasets

Turn on assistant_only_loss: true so loss is computed only on assistant turns — but confirm your chat template exposes {% generation %} blocks (e.g. Unsloth’s llama-3.1, gemma-2, qwen-2.5). Templates without them cannot produce the mask and TRL will error.


Related