Get API key

Fine-tuning

One call to fine-tune a model with LoRA or QLoRA: the SDK validates the config and the dataset locally, then runs the training job for you.

pm.finetune(...) is a supervised fine-tuning (SFT) job in one call. You name a base model, a JSONL dataset, and where the adapter should land; the SDK validates the configuration and the dataset before anything is submitted, ships a TRL/PEFT training recipe to the cluster, and returns the same Run handle as any other training job.

Nothing about it is a different subsystem. It submits a normal training job, tracked in MLflow, on the same GPU allocation, with the same wait(), watch(), refresh() and cancel().

Everything on this page runs as-is inside a PrivateMind GPU workspace. Configuration, authentication, and the exception hierarchy live on the SDK overview.

A first run

Python
import privatemind as pm

run = pm.finetune(
    model="Qwen/Qwen3-8B-Instruct",          # HF id, or a path on a Volume
    dataset="sft_data.jsonl",                # readable here, for the pre-flight check
    output_path="/workspace/adapters/support-v1",
    method="qlora",                          # 4-bit nf4 base loading
    experiment="support-bot",
)

run.wait()
print(run.phase)

Every argument is keyword-only. Three are required — model, dataset, output_path — and the rest have defaults tuned for a first run: method="lora", lr=2e-4, epochs=3, max_seq_len=4096, batch size 4 with 4 gradient accumulation steps, packing=True, seed=42.

Check before you submit

pm.finetune_dry_run(...) takes the same configuration fields, runs the same validation, and prints the resolved configuration without submitting anything:

Python
pm.finetune_dry_run(
    model="Qwen/Qwen3-8B-Instruct",
    dataset="sft_data.jsonl",
    output_path="/workspace/adapters/support-v1",
    method="qlora",
)
Text
privatemind finetune (dry run — no job submitted)
  model:        Qwen/Qwen3-8B-Instruct
  task/method:  sft / qlora (4-bit nf4 quantization)
  dataset:      sft_data.jsonl (job-side: /workspace/sft_data.jsonl) (chat_jsonl, validated 50 rows)
  output:       /workspace/adapters/support-v1 (adapter directory, written on job completion)
  experiment:   support-bot
  resources:    1 worker x 1 GPUs
  training:     lr=0.0002 epochs=3 max_seq_len=4096 packing=True batch=4x4 (effective 16) seed=42
  lora:         r=16 alpha=32 dropout=0.05 targets=q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj

It accepts the configuration fields plus workers, gpus_per_worker, validate_data and sniff_rows, and returns the validated FinetuneConfig. Cluster-side arguments (image, volumes, env and the rest) belong to finetune only. You can also build the config yourself and pass it as config=:

Python
from privatemind import FinetuneConfig, LoraConfig

cfg = FinetuneConfig(
    model="Qwen/Qwen3-8B-Instruct",
    dataset="sft_data.jsonl",
    output_path="/workspace/adapters/support-v1",
    lora=LoraConfig(r=32, alpha=64),
)
pm.finetune_dry_run(config=cfg)

FinetuneConfig and LoraConfig are frozen and reject unknown fields, so a typo raises ValidationError naming every problem at once rather than the first one.

Datasets

Two formats are registered today; pm.dataset_formats() returns their names.

Format One line looks like
chat_jsonl {"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
text {"text": "..."}

The file must be .jsonl (or .json holding one object per line). The format is detected from the first row unless you pass dataset_format=. Parquet is not supported yet and says so.

Before submit, the SDK reads the first 50 rows and validates each one against the format — sniff_rows= changes the count. A malformed line, a row that is not an object, an empty content, a file that is not UTF-8: each raises ValidationError naming the row, the reason, and an example of the shape it expected, so a mis-shaped dataset costs you a second rather than a GPU allocation.

If your columns are named something else, map them with column_map, which maps the canonical field to your actual column:

Python
run = pm.finetune(
    model="Qwen/Qwen3-8B-Instruct",
    dataset="sft_data.jsonl",
    output_path="/workspace/adapters/support-v1",
    column_map={"messages": "conversation"},
)

Pass validate_data=False to skip the pre-flight entirely — the case for it is a dataset that only exists on the cluster, on a Volume this notebook has not mounted. The path is then sent verbatim and must already be the path the job will see.

Set validation_ratio= (below 0.5) to hold out an evaluation split, seeded by seed so every rank splits identically. Leave it at 0 and the job trains on everything.

Where paths point

A dataset or a model path under your home Volume is translated automatically to the location the job sees. Write the path as you would in the notebook; the SDK rebases it before staging.

output_path is different: it is written by the job, so it must be an absolute path in the job's world — under /workspace for the home Volume, or under a mount_path you pass in volumes=. If it sits outside every mount, finetune warns and submits anyway, because pod-local disk is discarded when the job ends and the adapter would go with it.

A local path that lives outside the home Volume raises rather than warns: the job could never read it. An HF model id is passed through untouched.

Method and hyperparameters

method="lora" trains adapters on the base model in bf16 (fp16 where the GPU has no bf16). method="qlora" loads the base model in 4-bit nf4 with double quantization first, which is what makes a large base fit on a single GPU. Only task="sft" exists today.

Field Default Accepted
lr 2e-4 greater than 0, up to 0.1
epochs 3.0 greater than 0
max_seq_len 4096 128–131072
per_device_batch_size 4 1–256
gradient_accumulation_steps 4 1–1024
packing True —
validation_ratio 0.0 0.0–0.5 (exclusive)
seed 42 —

The adapter itself is configured through lora=, either a LoraConfig or a plain mapping of its fields:

Field Default Accepted
r 16 1–256
alpha 32 1 or more
dropout 0.05 0.0–1.0 (exclusive)
target_modules the seven attention and MLP projections any non-empty list of module names

packing concatenates short samples to fill the sequence length, which is a large throughput win on conversational data. It needs a FlashAttention implementation in the image so that packed samples do not attend across each other's boundaries; the job refuses to start without one rather than train on quietly wrong attention. Pass packing=False if your image has neither.

Resources

This release is single-node: workers must be 1, and gpus_per_worker must be at least 1. For multiple GPUs raise gpus_per_worker — the job launches one process per GPU with accelerate and trains under DDP.

target_cluster and home_volume default from the workspace context, and image resolves against the platform's own training-image allowlist, so in a notebook you pass neither. Outside a workspace the image resolves the same way, but the other two have no default: pass both, or the call raises ConfigError. cpu_per_worker, memory_per_worker, env, ray_version, ttl_seconds_after_finished, name, volumes, gpu_placements and client behave exactly as they do there. Environment variable names beginning PRIVATEMIND_FINETUNE_ are how the SDK wires the job and are rejected if you set them.

What the image must carry

The recipe runs inside the training image, and the platform's own images carry what it needs: transformers, peft, trl, accelerate, datasets and bitsandbytes, with flash-attn for packing. That is true from workspace-pytorch-ngc 1.4.0 onward, which is what image resolves to by default.

It matters if you bring your own image. The job's first log lines are a version and CUDA report, and a missing or broken dependency stops the run there, before any weights load, naming what is absent. A chat_jsonl dataset also needs a base model whose tokenizer carries a chat template, which in practice means an instruction-tuned model; that too is checked before any weights are loaded.

Tracking and output

Tracking is on by default. The run is named after the job, the resolved configuration is logged as parameters, the trainer's own metrics stream to it during training, and the finished adapter directory is attached as an artifact under adapter. Pass experiment= to group runs, or mlflow=False to turn tracking off. Reading metrics, parameters and artifacts back into the notebook is covered in Reading results back and needs pip install 'privatemind[mlflow]'.

On completion output_path holds the adapter — adapter_config.json, adapter_model.safetensors, and the tokenizer files — on the Volume, so it outlives the job.

Where next

  • Training jobs: the Run handle, watching progress, mounting data, and promoting a notebook function.
  • SDK overview: configuration, authentication, and the full error catalog.
  • Compute & training: what the platform offers and how it fits together.
  • Workspaces: the GPU notebook environment where the SDK is pre-configured.