n4nAI

Fine-tuning Llama 3: what you need to know

A practical guide to fine-tuning Llama 3 models — data prep, LoRA config, training loops, and deployment tradeoffs for engineers building production systems.

n4n Team7 min read1,559 words

Audio narration

Coming soon — every post will get a voice note here.

Fine-tuning Llama 3 isn’t magic, but it’s easy to waste GPU hours on the wrong knobs. This guide walks through the decisions that actually move the needle: data quality over quantity, LoRA ranks that fit your VRAM, and evaluation that catches regression before production. You’ll leave with a reproducible recipe and a mental model for when fine-tuning beats prompting, RAG, or a larger base model.

Start with the question you’re actually trying to answer

Before you spin up a single GPU, write down the failure mode of your current approach. Are you fighting format adherence? Domain vocabulary? Style and tone? A specific reasoning pattern? Each maps to a different data strategy and a different risk profile.

If the problem is “the model doesn’t know our internal acronyms,” fine-tuning is overkill — use RAG or a system prompt with a glossary. If the problem is “the model hallucinates our API signatures,” you need supervised fine-tuning (SFT) on verified examples. If it’s “the model refuses valid requests because of overzealous safety alignment,” you need preference optimization (DPO/ORPO) on your policy, not just more SFT.

Pitfall: Teams often fine-tune for knowledge injection. Don’t. Llama 3’s parametric knowledge is frozen at training cutoff. Fine-tuning teaches behavior, not facts. For facts, use retrieval.

Choose your model variant and quantization

Llama 3 ships as 8B and 70B parameter models, each in base and instruct flavors. For most teams, the choice is practical:

Variant VRAM (BF16) VRAM (4-bit QLoRA) Use case
8B Instruct ~16 GB ~6 GB Default starting point; fits single 24 GB GPU
70B Instruct ~140 GB ~48 GB Need stronger reasoning; requires multi-GPU or A100 80 GB
8B Base ~16 GB ~6 GB Continued pre-training or heavy domain adaptation
70B Base ~140 GB ~48 GB Rarely justified unless you have massive compute budget

Start with Llama-3-8B-Instruct quantized to 4-bit NF4 via QLoRA. It fits on a single consumer GPU (RTX 3090/4090, 24 GB) with room for batch size 2–4 and gradient accumulation. The instruct version already understands chat formatting, tool calling, and basic safety — you’re teaching your task, not chat basics.

# bitsandbytes 4-bit NF4 config — copy-paste ready
from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

Tradeoff: 4-bit QLoRA adds ~0.5–1% perplexity vs. BF16 full fine-tune, but reduces VRAM 4×. For 8B, that’s the difference between “runs on one GPU” and “needs two.” Take the hit.

Curate data like it’s production code

Your dataset is the model. A 1,000-example dataset of verified, diverse, correctly formatted conversations beats 50,000 scraped examples with hallucinations, truncated turns, and format drift.

Format: chat template compliance

Llama 3 uses a specific chat template with <|begin_of_text|>, <|start_header_id|>user<|end_header_id|>, <|eot_id|> delimiters. Your training data must match this exactly, or the model learns to ignore the template.

{
  "messages": [
    {"role": "system", "content": "You are a precise API documentation assistant."},
    {"role": "user", "content": "How do I authenticate to the payments endpoint?"},
    {"role": "assistant", "content": "Use the `Authorization: Bearer <token>` header. Token format: `sk_live_...` or `sk_test_...`."}
  ]
}

Apply the tokenizer’s apply_chat_template with tokenize=False to verify rendering before training. A single missing <|eot_id|> cascades into generations that never stop.

Quality filters worth automating

def filter_example(example, tokenizer, max_tokens=4096):
    # 1. Length guard
    rendered = tokenizer.apply_chat_template(
        example["messages"], tokenize=False
    )
    if len(tokenizer(rendered).input_ids) > max_tokens:
        return False
    
    # 2. Role alternation (no consecutive user/user or assistant/assistant)
    roles = [m["role"] for m in example["messages"]]
    if any(roles[i] == roles[i+1] for i in range(len(roles)-1)):
        return False
    
    # 3. Assistant message present (SFT needs targets)
    if roles[-1] != "assistant":
        return False
    
    # 4. Heuristic: reject if assistant response < 10 chars or > 2000 chars
    assistant_text = example["messages"][-1]["content"]
    if len(assistant_text) < 10 or len(assistant_text) > 2000:
        return False
    
    return True

Diversity over volume

Cover the distribution of your production inputs: short queries, multi-turn context, edge cases, adversarial prompts, different personas. If 80% of your traffic is “summarize this ticket,” 80% of your data should be summarization — but the remaining 20% must include the weird stuff or the model overfits the happy path.

Pitfall: Copy-pasting the same system prompt into every example. Vary it. Production system prompts change; the model should be robust to that.

LoRA configuration: the knobs that matter

For Llama 3 8B at 4-bit, this config is a strong default:

from peft import LoraConfig, TaskType

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,                    # rank — 8, 16, 32, 64
    lora_alpha=32,           # scaling = alpha / r
    lora_dropout=0.05,       # dropout on LoRA adapters
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",  # attention
        "gate_proj", "up_proj", "down_proj",     # MLP
    ],
    bias="none",
    modules_to_save=None,    # set to ["embed_tokens", "lm_head"] if adding tokens
)

Rank (r) and alpha: the capacity tradeoff

  • r=8, alpha=16 — minimal capacity, fastest, lowest overfit risk. Good for style/format tuning.
  • r=16, alpha=32 — sweet spot for most SFT tasks. ~0.5% of base params trained.
  • r=32, alpha=64 — more capacity for complex reasoning or multi-task. Watch for overfit on small datasets.
  • r=64, alpha=128 — approaching full fine-tune capacity. Needs more data, more regularization.

Rule of thumb: alpha = 2 * r keeps effective learning rate stable across ranks. Don’t treat alpha as a separate hyperparameter.

Target modules: don’t skip the MLP

Early LoRA papers targeted only attention. For Llama 3, include the MLP projections (gate_proj, up_proj, down_proj). The SwiGLU feed-forward layers carry significant task-specific adaptation. Skipping them saves ~30% adapter params but costs 1–2 points on benchmarks.

Modules_to_save: when you need it

If you add special tokens (new control codes, domain-specific markers), set modules_to_save=["embed_tokens", "lm_head"] so the new embeddings train. Otherwise leave it None — training the full vocab head is wasteful and destabilizes generation.

Training loop: stability over speed

from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./llama3-8b-lora",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,   # effective batch = 8
    num_train_epochs=3,
    learning_rate=2e-4,              # 1e-4 to 5e-4 for LoRA
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    weight_decay=0.01,
    max_grad_norm=1.0,
    bf16=True,                       # requires Ampere+ (RTX 30-series, A100, H100)
    tf32=True,
    gradient_checkpointing=True,     # saves ~30% VRAM, ~20% slower
    optim="paged_adamw_8bit",        # 8-bit optimizer states
    logging_steps=10,
    save_strategy="epoch",
    evaluation_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="eval_loss",
    greater_is_better=False,
    report_to="tensorboard",
    seed=42,
    data_seed=42,
)

Learning rate: the most sensitive knob

  • Too high (5e-4+): Loss spikes, generation degrades to gibberish within 500 steps.
  • Too low (5e-5): Underfits, needs 2× epochs to converge.
  • 2e-4 works for 90% of SFT on Llama 3 8B with r=16. If using r=64, drop to 1e-4.

Epochs vs. steps: stop early

Three epochs is a ceiling, not a target. Monitor eval loss every 50–100 steps. Stop when it plateaus or ticks up. Most 1k–5k example datasets converge in 1–2 epochs. More epochs = overfit to training format, not your task.

Pitfall: Training until training loss hits zero. That’s memorization, not learning. You want the gap between train and eval loss to stay small.

Gradient checkpointing + paged optimizer = single GPU viability

gradient_checkpointing=True recomputes activations on backward pass. optim="paged_adamw_8bit" offloads optimizer states to CPU. Together they make 8B QLoRA fit in 16–18 GB VRAM with batch size 2. Without both, you OOM.

Evaluation: don’t ship on vibes

You need three evaluation layers, and “it looks good to me” is none of them.

1. Automatic metrics (fast, noisy)

  • Eval loss — necessary but insufficient. Correlates weakly with downstream quality after ~1.5 perplexity.
  • ROUGE/BLEU — only valid for extractive tasks (summarization, translation). Useless for open-ended generation.
  • Format compliance rate — % of generations that parse as valid JSON / match regex / contain required tokens. Automatable, high signal for structured output tasks.
def format_compliance(generations, schema):
    """Returns fraction of generations matching JSON schema."""
    import json, jsonschema
    valid = 0
    for gen in generations:
        try:
            parsed = json.loads(extract_json(gen))
            jsonschema.validate(parsed, schema)
            valid += 1
        except Exception:
            pass
    return valid / len(generations)

2. LLM-as-judge (scalable, calibrated)

Use a strong model (GPT-4o, Claude 3.5 Sonnet, or a large open model) to score your fine-tuned outputs against a rubric. Pairwise comparison (fine-tuned vs. base instruct) is more reliable than absolute scoring.

JUDGE_PROMPT = """Compare two responses to the same prompt. 
Score 1 if Response A is better, -1 if Response B is better, 0 if tie.
Criteria: {criteria}

Prompt: {prompt}
Response A: {response_a}
Response B: {response_b}

Output only: 1, 0, or -1"""

Run 200–500 eval prompts. Bootstrap confidence intervals. If the fine-tuned model doesn’t beat base instruct with p < 0.05, don’t deploy it.

3. Human eval on the critical path (non-negotiable)

Sample 50–100 generations from your actual production distribution (not the eval set). Have domain experts rate: correctness, safety, tone, format. Track regression on a fixed “golden set” across every fine-tune run.

Pitfall: Evaluating only on the training distribution. Your eval set must include OOD prompts — typos, adversarial injections, multi-turn context, missing info — because that’s where fine-tuned models fail differently than base models.

Merge, quantize, serve

Merge LoRA weights for inference

from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B-Instruct",
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
model = PeftModel.from_pretrained(base_model, "./llama3-8b-lora/checkpoint-best")
model = model.merge_and_unload()  # fuses LoRA into base weights
model.save_pretrained("./llama3-8b-merged")
tokenizer.save_pretrained("./llama3-8b-merged")

Merged model = no PEFT dependency at inference, no adapter loading latency, standard generate() API.

Quantize for serving

For production throughput, quantize the merged model to AWQ 4-bit or GPTQ 4-bit (not the QLoRA NF4 used in training — different calibration).

# AWQ via autoawq — faster inference on GPU
python -m awq quantize \
  --model_path ./llama3-8b-merged \
  --quant_path ./llama3-8b-awq \
  --w_bit 4 --q_group_size 128 --zero_point --version GEMM

AWQ 4-bit typically retains 99%+ of BF16 quality at 2–3× throughput. Profile your specific workload — batch size, sequence length, KV cache pressure — before committing.

Serving stack

  • vLLM — best throughput for batched inference, PagedAttention, OpenAI-compatible API.
  • TGI (Text Generation Inference) — better for streaming, lower latency at batch=1, built-in watermarking.
  • llama.cpp / ollama — CPU/edge deployment, not for high-throughput GPU serving.
# vLLM OpenAI-compatible endpoint — drop-in for most clients
from vllm import LLM, SamplingParams

llm = LLM(
    model="./llama3-8b-awq",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.9,
    max_model_len=8192,
    quantization="awq",
)

outputs = llm.generate(
    prompts=["<|begin_of_text|><|start_header_id|>user<|end_header_id|>Hello<|eot_id|><|start_header_id|>assistant<|end_header_id|>"],
    sampling_params=SamplingParams(temperature=0.7, top_p=0.9, max_tokens=512),
)

Common failure modes and how to debug them

Symptom Likely cause Fix
Generates assistant<|end_header_id|> repeatedly Chat template mismatch in training data Re-verify apply_chat_template on every example
Loss drops but eval quality degrades Overfitting to format, not task Reduce epochs, increase dropout, add data diversity
Hallucinates new API endpoints Knowledge injection attempt via SFT Switch to RAG; fine-tune only for calling convention
Safe refusals on valid requests Over-alignment from instruct base DPO on preference pairs (accept vs. refuse)
KV cache OOM at batch > 4 max_model_len too high Set max_model_len=4096 or use sliding window
Throughput 10× lower than benchmark Running BF16 merged model, not quantized Deploy AWQ/GPTQ; verify quantization arg in vLLM

When to stop fine-tuning and try something else

Fine-tuning has diminishing returns. Consider alternatives when:

  • Data < 500 high-quality examples — few-shot prompting with a larger model (70B or 400B+) often wins.
  • Task changes weekly — RAG + prompt engineering adapts faster than retraining.
  • Need factual accuracy on evolving corpus — fine-tuning bakes in stale knowledge; retrieval doesn’t.
  • Latency budget < 100ms — even quantized 8B struggles; consider distillation to 1B–3B or a specialized encoder.
  • Multi-tenant with per-customer behavior — LoRA adapters per customer (swappable at inference) scale better than per-customer full fine-tunes.

Checklist before you call it done

  • Eval loss plateaued, train/eval gap < 0.1
  • LLM-as-judge shows statistically significant win over base instruct
  • Human eval on 50+ production-distribution samples: no regressions on safety, format, correctness
  • Format compliance rate > 99% on structured output tasks
  • Merged + AWQ model loads and generates in vLLM/TGI
  • Latency/throughput meets SLA at target batch size
  • Golden set regression test automated in CI
  • Rollback plan: base instruct model tagged and deployable in < 5 min

Fine-tuning Llama 3 is a lever, not a magic wand. The engineers who ship reliable models treat data as code, evaluation as testing, and deployment as a first-class concern — not an afterthought. Start small, measure ruthlessly, and only scale the recipe once the numbers justify it.

Tagsfine-tuningllama-3guidellm

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All fine-tuning fundamentals posts →