CloudCodeTree LogoCloudCodeTree
AI NewsTutorialsAbout
CloudCodeTree Logo
CloudCodeTree
  • AI News
  • Tutorials
  • About
← Back to AI News
Your Fine-Tune Might Be Lying to You: Measure It With lm-evaluation-harness

Your Fine-Tune Might Be Lying to You: Measure It With lm-evaluation-harness

Chris Harper

4 min read

Aug 15, 2026 · 12:04 UTC

AI
Tutorial
Fine-Tuning
HuggingFace

Training loss drops — but your fine-tuned model can still quietly break on everything it wasn't tested on. Three layers of evaluation catch regressions before production does.

What you'll be able to do after this:

  • Detect catastrophic forgetting before shipping a fine-tuned model
  • Run standard benchmarks (MMLU, ARC, GSM8K) on any HuggingFace model with one command using lm-evaluation-harness
  • Know when to use benchmark evals vs task-specific evals vs LLM-as-judge

The hidden trap

Training loss goes down. The model responds fluently. But did it improve on your task — without quietly regressing on everything else? Most devs skip evaluation and ship on vibes. The risk is catastrophic forgetting: a model fine-tuned on customer support data might score 10–15 points lower on math reasoning (GSM8K) after just two epochs, with no warning in the training logs.

Three evaluation layers catch this before users do.

Layer 1: Your held-out eval set (task accuracy)

Before training, set aside 10–20% of your dataset as a held-out test set. Score your fine-tuned model against it after training:

from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_id = "./outputs/my-fine-tuned-model"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id, torch_dtype=torch.float16, device_map="auto"
)

test_data = load_dataset("json", data_files="test.jsonl")["train"]

correct = 0
for example in test_data:
    inputs = tokenizer(example["prompt"], return_tensors="pt").to("cuda")
    with torch.no_grad():
        output = model.generate(**inputs, max_new_tokens=256)
    response = tokenizer.decode(output[0], skip_special_tokens=True)
    if example["expected"] in response:
        correct += 1

print(f"Task accuracy: {correct/len(test_data)*100:.1f}%")

For structured outputs (JSON, SQL, code), use exact-match. For freeform text, use ROUGE or BERTScore. This is your primary signal.

Layer 2: Benchmark regression with lm-evaluation-harness

lm-evaluation-harness (EleutherAI) is the framework behind the HuggingFace Open LLM Leaderboard — the standard infrastructure for evaluating models at benchmark scale. One command runs your model on 200+ tasks. Run it on the base model first, save the results, then compare after fine-tuning.

pip install lm-eval

# Step 1: baseline the base model
lm_eval --model hf \
  --model_args pretrained=meta-llama/Llama-3.1-8B-Instruct \
  --tasks mmlu,gsm8k,arc_challenge \
  --device cuda:0 --batch_size 8 \
  --output_path ./eval-results/base

# Step 2: eval your fine-tuned model with the same tasks
lm_eval --model hf \
  --model_args pretrained=./outputs/my-fine-tuned-model \
  --tasks mmlu,gsm8k,arc_challenge \
  --device cuda:0 --batch_size 8 \
  --output_path ./eval-results/fine-tuned

Then diff the result JSON files. What to watch:

  • MMLU: general knowledge across 57 subjects — a broad regression signal
  • GSM8K: grade-school math — catches reasoning degradation fast
  • ARC-Challenge: science questions requiring multi-step reasoning

Interpreting the delta:

GSM8K / MMLU dropVerdict
0–3%Normal — fine-tuning always trades some generality
3–10%Investigate — check training epochs, learning rate, data mix
10%+Catastrophic forgetting — roll back; revisit LoRA rank, dataset balance, or add a regularization term

Layer 3: LLM-as-judge for generative quality

Automated metrics can't assess response quality for open-ended tasks. For that, use a frontier model as a judge:

import anthropic, json

client = anthropic.Anthropic()

def judge_response(prompt: str, response: str, rubric: str) -> dict:
    result = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": f"""Rate this response on a 1–5 scale.

Rubric: {rubric}

User prompt: {prompt}
Model response: {response}

Return JSON only: {{"score": 1-5, "reason": "one sentence"}}"""
        }]
    )
    return json.loads(result.content[0].text)

# Run on 50–100 held-out examples
results = [
    judge_response(ex["prompt"], generate(model, ex["prompt"]), rubric)
    for ex in eval_set
]
avg_score = sum(r["score"] for r in results) / len(results)
print(f"LLM-judge avg score: {avg_score:.2f}/5.0")

Run this against your base model too to get a baseline. A fine-tune that scores below the base on the judge is a regression.

Which layer for which task

Task typeLayer 1Layer 2Layer 3
Structured output (JSON, SQL, code)Exact-matchAlwaysOptional
Freeform generation (chat, summaries)ROUGEAlwaysRequired
Any fine-tuneRequiredRequiredRecommended

Sources: lm-evaluation-harness — EleutherAI/GitHub · lm-eval-harness quickstart — readthedocs.io · Evaluate Fine-Tuned LLMs: MMLU, MT-Bench, Custom Evals — markaicode.com