A small web UI to fine-tune MLX (mlx-lm) language models on Apple Silicon
Macs. Upload a dataset, kick off a LoRA/DoRA training run, watch live logs via
WebSocket, and chat with the resulting model.
- Backend: FastAPI +
mlx-lm(LoRA viamlx_lm.lora.run). Python deps managed withuv. - Frontend: Next.js (App Router) + Tailwind v4. Talks to the backend over
/api/*(proxied via Next rewrites) and/ws/runs/:idfor live logs.
backend/
app/
main.py FastAPI app: upload, train, runs, chat, WebSocket
trainer.py mlx-lm LoRA training orchestration (stdout-captured)
inference.py load base + adapter, generate completions
datasets.py .jsonl/.json validation
runs.py in-memory run registry
config.py settings (data dirs)
pyproject.toml uv-managed deps
frontend/
src/app/page.tsx Train tab + Runs tab + chat panel
src/lib/api.ts API client + WS helper
next.config.ts rewrites /api -> backend
scripts/dev-backend.sh
- Apple Silicon Mac (M-series).
uv(Python),pnpm(Node), Node 20+.- Internet access the first time you train (to pull a model snapshot from the Hugging Face Hub).
# backend (creates .venv, installs mlx, mlx-lm, fastapi, ...)
cd backend
uv sync
# frontend
cd ../frontend
pnpm installTwo terminals:
# 1) backend (reads backend/.env for EASYTRAIN_HOST / EASYTRAIN_PORT)
cd backend
uv run python -m app.main
# 2) frontend (reads frontend/.env.local for BACKEND_URL)
cd ../frontend
pnpm devOpen http://localhost:3000. Defaults assume the backend is at
http://127.0.0.1:8765 (see backend/.env and frontend/.env.local). To run the
backend on the default FastAPI port 8000 instead, edit backend/.env and
frontend/.env.local accordingly.
.jsonl (one JSON object per line) with either:
{"text": "...raw text..."}(causal LM), or{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}(chat).
.json files with a {"train": [...]} shape are also accepted. Validation
runs on upload; a per-run train.jsonl is staged into backend/data/datasets/
for mlx_lm.
The backend calls mlx_lm.lora.run() (the same code path as python -m mlx_lm lora --train ...) with an argparse-style namespace built from the UI config.
adapter_config.json + adapters.safetensors are written to
backend/data/checkpoints/<run_id>/. Stdout/stderr is captured line-by-line
and exposed via GET /api/runs/<id> and WS /ws/runs/<id>.
POST /api/runs/<id>/chat loads the base model with the run's adapter
(mlx_lm.load(model, adapter_path=...)) and generates with the model's chat
template if available.
- Runs are kept in memory (no DB); restarting the backend clears the registry.
- Adapters stay on disk under
backend/data/checkpoints/until deleted viaDELETE /api/runs/<id>. fullfine-tune type trains all weights — needs significantly more memory.- The first chat after training re-loads the model (cold); subsequent runs in the same backend process benefit from any HF cache.