Python FastAPI backend that runs LoRA supervised fine-tuning (TRL SFTTrainer + PEFT) on OpenAI-style messages JSONL, plus a Next.js UI to validate data, preview configuration, start a single local job at a time, stream logs (SSE), cancel runs, and download a zip of artifacts.
GGUF conversion and Ollama import are documentation-only in this repo (see appendix).
- Python 3.10+ (3.11 recommended). This repo was tested with a local venv under
backend/.venv. - PyTorch installed for your platform (pytorch.org — pick CUDA, CPU, or MPS).
- Node.js LTS and npm (for the frontend; see
frontend/package.jsonfor pinned Next.js / React versions). - Disk space for the base model (often many GB), uploaded datasets, and checkpoints.
- A local Hugging Face–style model folder (
config.json+ weights) forbase_model_path. Download with Hugging Face tooling or copy from elsewhere; the UI does not ship multi‑GB base weights for you.
From the repo root:
cd backend
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtrequirements.txt includes rich (required by recent trl imports). If pip install torch fails on your OS, follow PyTorch’s official install command first, then install the rest.
cd frontend
npm install- Root
.env.example— copy tobackend/.env(or export vars) before running uvicorn frombackend/(Pydantic loads.envfrom the process working directory). Key fields:DATA_DIR— where jobs, uploads, and artifacts live (default./data).CORS_ORIGINS— comma-separated origins; include your Next.js origin (defaulthttp://localhost:3000).USE_FAKE_TRAINER— set to1for a dry/demo run that writes placeholder artifacts without loading TRL training (useful for wiring tests).ENABLE_JOB_PLAYGROUND— set to0to disablePOST /jobs/{id}/playground/compareand hide the job-page compare UI (RAM-heavy; loads checkpoints per request).MODEL_PRESETS_SCAN_DIR— optional directory (path is resolved relative to the backend cwd) whose subfolders containingconfig.jsonappear as base model shortcuts in the UI viaGET /system/model-shortcuts.BASE_MODEL_SHORTCUTS— optional JSON array of explicit shortcuts, e.g.[{"label":"Llama 8B","path":"/abs/path/to/model"}]. Entries are merged with scan results; invalid JSON is surfaced in the API response’sparse_errorfield.
frontend/.env.local— copy fromfrontend/.env.example; setNEXT_PUBLIC_API_URLto the API base (e.g.http://localhost:8000).
Terminal 1 — API
cd backend
source .venv/bin/activate
export DATA_DIR="$(pwd)/../data" # optional explicit path
export USE_FAKE_TRAINER=0 # set to 1 for demo trainer
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Terminal 2 — UI
cd frontend
npm run devOpen http://localhost:3000. The home page loads GET /system/capabilities (CUDA / MPS / CPU, best‑effort VRAM on CUDA, warnings) and GET /system/model-shortcuts (preset base models from MODEL_PRESETS_SCAN_DIR / BASE_MODEL_SHORTCUTS). GET /health returns {"status":"ok"} for simple uptime checks.
- Upload —
POST /datasets/upload(multipart file) stores underDATA_DIR/uploads/and returns a relativepath, or setdataset_pathmanually to a file underDATA_DIR(e.g.uploads/<id>.jsonl). Allowed upload suffixes are.jsonlor.json; content must still be JSON Lines (one chat record per line), same as validation expects. POST /datasets/validate(or Validate sample in the UI) — parses the first N lines, checksmessagesroles/content, optional tokenizer length estimate whenbase_model_pathis set.- Fill
base_model_path(absolute path on the machine running the API) and hyperparameters. Use shortcuts from the UI when configured. - Preview / validate —
POST /jobs/previewruns the same validators plus device/dtype warnings. - Approve & start —
POST /jobscreates the job and starts training. Only one non‑terminal job is allowed globally (409withactive_job_idif busy). - Job page streams
GET /jobs/{id}/events(SSE), pollsGET /jobs/{id}, and loads loss curves fromGET /jobs/{id}/training-metrics(backed byoutput/metrics.jsonlwhen present). Cancel viaPOST /jobs/{id}/cancel(cooperative stop). - When finished,
GET /jobs/{id}/artifacts.zipcontainsconfig.resolved.json, logs, andoutput/(adapter underoutput/adapter/, optional merged HF model underoutput/merged/, plusoutput/ollama/— Modelfiles and README for optional GGUF/Ollama export). The job UI callsGET /jobs/{id}/ollama-recipefor that recipe as JSON.POST /jobs/{id}/playground/comparepowers the job-page side‑by‑side text boxes (Transformers inference on the API host — no GGUF required).GET /jobs/{id}/playground/hintsreturns the firstsystemmessage from the training JSONL (if any), which the UI can use when aligning the playground with dataset-style prompting.
One JSON object per line, OpenAI-style chat messages (roles: system, user, assistant, tool):
{"messages":[{"role":"user","content":"Say hello."},{"role":"assistant","content":"Hello!"}]}Each record must include at least one assistant message with non-empty content.
Under examples/:
examples/sample.jsonl— minimal JSONL you can upload or pointdataset_pathat (after copying underDATA_DIRor via upload).examples/nascar_company_dataset.jsonl— larger fictional retail/B2B chat set; regenerate withexamples/generate_nascar_company_dataset.py.
{
"job_name": "my-run",
"base_model_path": "/models/Meta-Llama-3-8B-Instruct",
"dataset_path": "uploads/abc123.jsonl",
"max_seq_length": 2048,
"lora_r": 16,
"lora_alpha": 32,
"lora_dropout": 0.05,
"lora_target_modules": null,
"learning_rate": 0.0002,
"num_train_epochs": 1,
"max_steps": null,
"per_device_train_batch_size": 1,
"gradient_accumulation_steps": 4,
"warmup_ratio": 0.03,
"logging_steps": 10,
"save_steps": "epoch",
"seed": 42,
"fp16": false,
"bf16": true,
"device_preference": "auto",
"merge_adapter_after_train": false
}Paths:
dataset_pathmust resolve underDATA_DIR(relative paths are rooted there).base_model_pathcan be any readable directory on the host withconfig.json(not restricted toDATA_DIR).
job_name must start with a letter or digit and may contain only letters, digits, ., _, and - (max 128 characters).
- CUDA OOM / MPS errors — lower
max_seq_length,per_device_train_batch_size, orlora_r; increasegradient_accumulation_steps; try fp16 instead of bf16 on MPS; enable CPU only as a last resort. - bf16 on CPU/MPS — may be unsupported or flaky; switch to fp32 (both
fp16andbf16false) or fp16 where appropriate. - Slow CPU training — expected; use a GPU or a smaller model for experimentation.
- 409 Active job — wait for the running job to finish, cancel it, or inspect
data/jobs/active.jsonunderDATA_DIRif the process crashed mid‑run (you may clear stale state manually in development). - Job playground & system prompts — In
POST /jobs/{id}/playground/compare, omittingsystem_prompt(or sending JSONnull) uses the firstsystemmessage found in the job’s training JSONL for both base and fine-tuned generations. An empty string uses no system message on either side. Any other string is used as the same explicit system prompt on both sides (useful for matching production prompts; a fixed system can mask what the adapter alone changed). The job UI defaults to no system (""); switch to dataset mode there to omit the field and match APInullbehavior. - Job playground OOM / slow —
POST /jobs/{id}/playground/comparereloads weights each request; use a smaller base model, lower max new tokens, or turn off bf16 / use fp32. SetENABLE_JOB_PLAYGROUND=0to hide the feature.
This project outputs Hugging Face–compatible weights (LoRA adapter and optionally merged full weights). Ollama typically consumes GGUF. Completed runs also write output/ollama/ and expose GET /jobs/{job_id}/ollama-recipe so you can copy Modelfiles and paired ollama run … commands for side‑by‑side testing (you still produce the .gguf files locally).
High-level steps (details depend on model family and llama.cpp support):
- Convert the base and merged HF checkpoints with llama.cpp tooling (see upstream docs and scripts such as
convert_hf_to_gguf.pyin the llama.cpp repository). - Create two Ollama models using Modelfiles — see Ollama’s Modelfile documentation for directives like
FROMpointing at a.gguffile (Ollama Modelfile docs).
There is no bundled GGUF conversion binary in this repo; conversion stays external.