I spent two weeks building is on HuggingFace, and it runs locally via Ollama in 30 seconds.
Total bill: ~$30. GPU time: ~2 hours. The data pipeline: most of those two weeks and 25+ documented decisions. That ratio is what this post is about.
The task
The judge takes a question from
| Metric | Baseline | SFT | SFT+DPO |
|---|---|---|---|
| Overall κ (in-dist) | 0.481 | 0.647 | 0.682 |
| Overall κ (OOD religion) | 0.542 | 0.695 | 0.643 |
| Subtle cases κ | 0.632 | 0.743 | 0.890 |
| Position-bias rate | 21.2% | 8.4% | 9.2% |
κ is Cohen's kappa — agreement with human labels above chance. The eval set: 240 in-distribution pairs + 60 from religion (held out entirely from training).
DPO improved in-dist κ modestly and dramatically improved subtle-bias detection (0.743 → 0.890). Position bias dropped from 21% to 9%.
But look at the OOD row. DPO made out-of-distribution performance worse. SFT generalizes to unseen bias categories (κ = 0.695) better than DPO (κ = 0.643).
The likely explanation: synthesized hard negatives in DPO encoded patterns specific to the 10 in-distribution categories. DPO learned to discriminate those patterns rather than bias-in-general. On an unseen category, the pattern-matching hurts.
SFT → DPO is not a monotonic improvement. DPO trades generalization breadth for in-distribution precision. I published .
What the training looked like
Deliberately brief, because that's the point.
QLoRA SFT: Unsloth + TRL, 3 epochs on 3,844 rows, r=16 LoRA, lr=2e-4, single A100, 88 minutes. DPO: 1 epoch on 2,200 rows, β=0.1, 20 minutes. Standard hyperparameters. Every parameter is , which gives $30/month in free credits — enough to cover this entire project's compute without paying anything.
Try it now
ollama run hf.co/krishnakartik/gemma4-social-bias-judge-gguf:Q8_0
Or via the OpenAI-compatible API:
curl http://localhost:11434/v1/chat/completions \
-d '{"model": "hf.co/krishnakartik/gemma4-social-bias-judge-gguf:Q8_0",
"messages": [{"role": "system", "content": "..."},
{"role": "user", "content": "..."}]}'
At scale, the self-hosted judge runs at 32× lower cost per judgment than the frontier model used to create its training data.
The takeaway
I built this with Claude as a collaborator — chat for pipeline design, Claude Code for staged implementation. The coding assistant wrote most of the implementation across 11 pipeline stages. The 25 decisions that determined whether the model was good or mediocre were mine.
The agent can write your training loop. It cannot decide what your training data should look like. That's where your time goes. Not tuning learning rates.
The full pipeline, prompts, and decision log: judge-from-scratch.
SOCIAL SHARE CARD GENERATOR