En este modulo

  1. When fine-tuning vs prompt engineering
  2. LoRA and QLoRA: the minimum necessary theory
  3. ChatML dataset preparation
  4. Training with Unsloth
  5. Training with Axolotl
  6. Hyperparameters that matter
  7. Evaluating the fine-tuned model
  8. Common fine-tuning mistakes
  9. Ejercicio practico
  10. Puntos clave

When fine-tuning vs prompt engineering

Fine-tuning is not the first option. It is the last. Before investing hours preparing datasets and training, try solving your problem with prompt engineering. The decision is pragmatic, not academic.

Use prompt engineering when

Use fine-tuning when

The 80/20 rule

If the base model with good prompting achieves >80% of the quality you need, fine-tuning will probably take you to 90-95%. If the base model is below 50%, fine-tuning won't save you. The problem is in the data or model choice, not in training.

LoRA and QLoRA: the minimum necessary theory

Full fine-tuning of a 27B model requires ~200GB of VRAM (weights + gradients + optimizer state). Impractical. LoRA (Low-Rank Adaptation) solves this by training only a small set of additional matrices (~1-5% of parameters), keeping the original weights frozen.

How LoRA works

In each attention layer of the transformer, LoRA inserts two small matrices (A and B) whose product is added to the original weights. The rank of these matrices (r) controls capacity: more rank = more trainable parameters = more adaptation capacity, but more VRAM and more overfitting risk.

# Intuition: LoRA decomposes weight updates
# W_original (4096 x 4096) = 16M parameters (frozen)
# LoRA A (4096 x r) + LoRA B (r x 4096)
# If r=16: 4096*16 + 16*4096 = 131K parameters (0.8% of original)

# QLoRA: LoRA + base model quantized to 4-bit
# The base model loads in 4-bit (saves VRAM)
# LoRA matrices train in FP16 (maintains precision)
# Result: fine-tuning a 27B model on a 24GB GPU

QLoRA: fine-tuning on accessible hardware

QLoRA combines base model quantization (4-bit NF4) with LoRA. The base model takes ~14GB for a 27B, and LoRA matrices + optimizer state add ~4-6GB. Total: ~20GB. Fits on an RTX 4090 or A100-40GB.

ChatML dataset preparation

The dataset is the most important component of fine-tuning. Mediocre models with good data outperform excellent models with bad data. Quality matters more than quantity.

ChatML format

# ChatML is the standard format for fine-tuning conversational models
# Each example is a complete conversation

# dataset.jsonl
{"conversations": [
  {"role": "system", "content": "You are a security analyst specialized in SOC alert triage."},
  {"role": "user", "content": "Alert: multiple failed login from IP 185.143.223.47 against admin user. 47 attempts in 2 minutes. Source: SIEM."},
  {"role": "assistant", "content": "## Triage\n\n**Severity:** High\n**Category:** Brute Force (T1110)\n**IOC:** 185.143.223.47\n\n### Analysis\n- 47 attempts in 2 minutes against privileged account indicates automated brute force attack\n- IP 185.143.223.47: European range, verify in threat intel\n\n### Recommended actions\n1. Block IP at perimeter firewall\n2. Verify if the account was compromised\n3. Review last 24h logs for this IP"}
]}

# Another example
{"conversations": [
  {"role": "system", "content": "You are a security analyst specialized in SOC alert triage."},
  {"role": "user", "content": "Alert: abnormal DNS traffic detected. Host 10.0.1.45 performing 3000 DNS queries to random domains in 5 minutes."},
  {"role": "assistant", "content": "## Triage\n\n**Severity:** Critical\n**Category:** DNS Tunneling / C2 (T1071.004)\n**IOC:** Host 10.0.1.45, DNS beaconing pattern\n\n### Analysis\n- 3000 queries in 5 min to random domains is a classic DNS tunneling or DGA (Domain Generation Algorithm) pattern\n- Possible data exfiltration or C2 communication\n\n### Recommended actions\n1. Isolate host 10.0.1.45 from the network immediately\n2. Capture memory and disk for forensic analysis\n3. Analyze queried domains against CTI feeds\n4. Review historical traffic for the host"}
]}

Dataset quality rules

  1. Format consistency: all responses must follow the same format. If you use Markdown headers, use them always. If you use severity as a field, it should always be present.
  2. Input diversity: cover a variety of scenarios. Not 500 brute force examples and 2 phishing ones.
  3. Output quality: each assistant response should be a perfect example of what you want the model to produce. If you wouldn't publish it as reference, don't include it.
  4. Appropriate length: neither too short (the model learns to be lazy) nor too long (the model learns to pad with filler).
  5. Consistent system prompt: use the same system prompt for all examples of the same task.

How many examples do you need

# Empirical rules for dataset size
# --------------------------------------------------
# Task                    Minimum examples    Ideal
# --------------------------------------------------
# Classification (3-5 cat)       200              1000
# Custom output format           500              2000
# Specialized domain            1000              5000
# Agent with tool calling       2000             10000
# --------------------------------------------------
# Beyond 10K, diminishing returns unless
# task variability is very high

Training with Unsloth

Unsloth is the fastest tool for LLM fine-tuning. It automatically optimizes memory usage and training speed, making fine-tuning 2-5x faster than standard HuggingFace Transformers.

# Installation
pip install unsloth

# Fine-tuning script with Unsloth + QLoRA
from unsloth import FastLanguageModel
import torch

# 1. Load base model in 4-bit
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="Qwen/Qwen3.5-27B",
    max_seq_length=4096,
    dtype=None,          # Auto-detect (float16 for A100, bfloat16 for H100)
    load_in_4bit=True,   # QLoRA: base model in 4-bit
)

# 2. Add LoRA adapters
model = FastLanguageModel.get_peft_model(
    model,
    r=16,                       # LoRA rank (16-64 typical)
    target_modules=[            # Which layers to adapt
        "q_proj", "k_proj", "v_proj", "o_proj",  # Attention
        "gate_proj", "up_proj", "down_proj",       # FFN
    ],
    lora_alpha=32,              # Scale factor (typical: 2*r)
    lora_dropout=0.05,          # Regularization
    bias="none",
    use_gradient_checkpointing="unsloth",  # Saves VRAM
)

# 3. Prepare dataset
from datasets import load_dataset

dataset = load_dataset("json", data_files="dataset.jsonl", split="train")

def format_chat(example):
    """Converts ChatML to text format for the tokenizer."""
    messages = example["conversations"]
    text = tokenizer.apply_chat_template(messages, tokenize=False)
    return {"text": text}

dataset = dataset.map(format_chat)

# 4. Train
from trl import SFTTrainer
from transformers import TrainingArguments

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=4096,
    args=TrainingArguments(
        output_dir="./output",
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,  # Effective batch = 2*4 = 8
        warmup_steps=10,
        num_train_epochs=3,
        learning_rate=2e-4,
        fp16=not torch.cuda.is_bf16_supported(),
        bf16=torch.cuda.is_bf16_supported(),
        logging_steps=10,
        save_strategy="epoch",
        optim="adamw_8bit",  # 8-bit optimizer (saves VRAM)
    ),
)

trainer.train()

# 5. Save model
model.save_pretrained("./qwen3.5-27b-soc-lora")
tokenizer.save_pretrained("./qwen3.5-27b-soc-lora")

# 6. Merge LoRA with base model (for serving with vLLM)
model.save_pretrained_merged(
    "./qwen3.5-27b-soc-merged",
    tokenizer,
    save_method="merged_16bit",  # Merge + save in FP16
)

Training with Axolotl

Axolotl is an alternative to Unsloth with more configuration options. It is configured via YAML, which facilitates reproducibility and version control of training configurations.

# axolotl_config.yml
base_model: Qwen/Qwen3.5-27B
model_type: AutoModelForCausalLM
tokenizer_type: AutoTokenizer

load_in_4bit: true
adapter: qlora
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules:
  - q_proj
  - k_proj
  - v_proj
  - o_proj
  - gate_proj
  - up_proj
  - down_proj

datasets:
  - path: dataset.jsonl
    type: sharegpt  # ChatML format
    conversation: chatml

sequence_len: 4096
sample_packing: true  # Packs multiple short examples into one sequence

num_epochs: 3
micro_batch_size: 2
gradient_accumulation_steps: 4
learning_rate: 2e-4
lr_scheduler: cosine
warmup_steps: 10
optimizer: adamw_bnb_8bit

output_dir: ./output
save_strategy: epoch
logging_steps: 10

bf16: auto
flash_attention: true
gradient_checkpointing: true
# Run training with Axolotl
pip install axolotl
accelerate launch -m axolotl.cli.train axolotl_config.yml

# Merge LoRA after training
python -m axolotl.cli.merge_lora axolotl_config.yml --lora_model_dir ./output/checkpoint-final

Hyperparameters that matter

Of the 20+ possible hyperparameters, these 5 determine 90% of the result:

Evaluating the fine-tuned model

# Evaluation: compare base model vs fine-tuned
import json
from openai import OpenAI

def evaluate_model(client, model_name, eval_path):
    results = []
    with open(eval_path) as f:
        for line in f:
            example = json.loads(line)
            messages = example["conversations"][:-1]  # Without assistant response
            expected = example["conversations"][-1]["content"]

            response = client.chat.completions.create(
                model=model_name,
                messages=messages,
                max_tokens=1024,
                temperature=0
            )
            predicted = response.choices[0].message.content

            # Scoring (adapt to your task)
            score = compute_score(predicted, expected)
            results.append(score)

    avg_score = sum(results) / len(results)
    print(f"{model_name}: {avg_score:.2%} ({len(results)} examples)")
    return avg_score

# Evaluate base
base_score = evaluate_model(base_client, "Qwen3.5-27B-AWQ", "eval_dataset.jsonl")

# Evaluate fine-tuned
ft_score = evaluate_model(ft_client, "qwen3.5-27b-soc-merged", "eval_dataset.jsonl")

print(f"Improvement: {ft_score - base_score:+.2%}")

Separate eval dataset

Never evaluate with the same data you used for training. Separate 10-20% of your dataset as eval set before starting. If you don't, you don't know whether the model learned or simply memorized.

Common fine-tuning mistakes

  1. Contaminated dataset: eval data in the train set. Inflated results. Separate before any preprocessing.
  2. Overfitting: loss drops to zero, but the model can only repeat training examples. Solution: fewer epochs, more dropout, or more data.
  3. Catastrophic forgetting: the model loses general capabilities after fine-tuning. Solution: use LoRA (preserves base weights), reduce learning rate, or mix general data with specific data.
  4. Inconsistent format: the dataset mixes output formats. The model doesn't know which format to produce. Solution: normalize all outputs to the same format before training.
  5. Low quality data: "garbage in, garbage out" applies especially to fine-tuning. 500 perfect examples beat 5000 mediocre ones.

Ejercicio practico

Ejercicio TE04: Fine-tuning a model
  1. Choose a specific task from your domain (ticket classification, document summarization, report generation).
  2. Create a dataset of 200+ examples in ChatML format. Separate 20% for eval.
  3. Train with Unsloth + QLoRA on Phi-4 14B (fits in 16GB VRAM) or Qwen3.5-7B.
  4. Evaluate: compare the base model vs fine-tuned on your eval set. Calculate the percentage improvement.
  5. Merge the LoRA and serve it with vLLM. Verify it works end-to-end.
  6. Experiment: train with r=8 and r=32. Train with 1 and 5 epochs. Document how quality changes.

If you don't have a GPU: Google Colab Pro (10 EUR/month) gives you access to T4/A100 sufficient for fine-tuning 7-14B models with QLoRA.

Puntos clave

Puntos clave from TE04

  1. Fine-tuning is the last option, not the first. Try prompt engineering first. Fine-tune when you need format consistency, specialized domain, or reduced latency (shorter prompts).
  2. QLoRA makes fine-tuning accessible: a 27B model trains on a 24GB GPU. Unsloth is the fastest tool; Axolotl is more configurable.
  3. The dataset is more important than the model. 500 perfect examples beat 5000 mediocre ones. Consistent format, diverse inputs, quality outputs.
  4. Always evaluate with separate data. Without an eval set, you don't know if the model learned or memorized.
  5. The critical hyperparameters are 5: learning rate (2e-4), LoRA rank (16), epochs (3), batch size (8), max sequence length. Start with these defaults and adjust.
Guia de estudio — Conceptos clave de TE04

Cuando fine-tuning vs prompt engineering

  • Tienes menos de 500 ejemplos de entrenamiento.
  • El formato de salida deseado se puede describir con instrucciones claras.
  • La tarea es generica (resumen, traduccion, clasificacion simple).
  • Necesitas iterar rapido (cambiar el comportamiento sin reentrenar).
  • Usas un modelo frontier via API (no puedes fine-tunear GPT-4o tu mismo de forma efectiva).
  • Tienes 1.000+ ejemplos de alta calidad y el modelo base no consigue la calidad deseada con prompting.

Preparacion de datasets ChatML

  • Severidad: Alta\n Categoria: Brute Force (T1110)\n IOC: 185.143.223.47\n\n### Analisis\n- 47 intentos en 2 minutos contra cuenta privilegiada indica ataque de fuerza bruta automatizado\n- IP 185.143.223.47: rango europeo, verificar en threat intel\n\n### Acciones recomendadas\n1. Bloquear IP en firewall perimetral\n2. Verificar si la cuenta fue comprometida\n3. Revisar logs de las ultimas 24h para esta IP"}
  • Severidad: Critica\n Categoria: DNS Tunneling / C2 (T1071.004)\n IOC: Host 10.0.1.45, patron de DNS beaconing\n\n### Analisis\n- 3000 queries en 5 min a dominios aleatorios es patron clasico de DNS tunneling o DGA (Domain Generation Algorithm)\n- Posible exfiltracion de datos o comunicacion C2\n\n### Acciones recomendadas\n1. Aislar host 10.0.1.45 de la red inmediatamente\n2. Capturar memoria y disco para analisis forense\n3. Analizar dominios consultados contra feeds CTI\n4. Revisar trafico historico del host"}
  • Consistencia de formato:todas las respuestas deben seguir el mismo formato. Si usas headers Markdown, usarlos siempre. Si usas severidad como campo, que siempre este presente.
  • Diversidad de inputs:cubrir variedad de escenarios. No 500 ejemplos de brute force y 2 de phishing.
  • Calidad de outputs:cada respuesta del assistant debe ser un ejemplo perfecto de lo que quieres que el modelo produzca. Si no lo publicarias como referencia, no lo incluyas.
  • Longitud apropiada:ni demasiado corto (el modelo aprende a ser perezoso) ni demasiado largo (el modelo aprende a rellenar con padding).

Entrenamiento con Axolotl

# Ejecutar entrenamiento con Axolotl pip install axolotl accelerate launch -m axolotl.cli.train axolotl_config.yml # Merge LoRA despues del entrenamiento python -m axolotl.cli.merge_lora axolotl_config.yml --lora_model_dir ./output/checkpoint-final

Hiperparametros que importan

  • Learning rate (2e-4 a 5e-5):demasiado alto = olvida conocimiento base. Demasiado bajo = no aprende. 2e-4 es un buen punto de partida para QLoRA.
  • LoRA rank (8, 16, 32, 64):mas rango = mas capacidad, mas VRAM, mas riesgo de overfit. 16 es el default seguro. Sube a 32-64 si tienes >5K ejemplos.
  • Epochs (1-5):mas epochs = mas riesgo de overfit. 3 epochs es tipico. Con<1K ejemplos, 1-2 epochs.
  • Batch size efectivo (4-16):micro_batch * gradient_accumulation. Valores mas altos estabilizan el entrenamiento. 8 es un buen default.
  • Max sequence length:debe cubrir tu input + output mas largo. Mas largo = mas VRAM. No pongas 8192 si tus ejemplos tienen 2000 tokens.

Evaluacion del modelo fine-tuned

  • Eval dataset separado: Nunca evalues con los mismos datos que usaste para entrenar. Separa 10-20% de tu dataset como eval set antes de empezar. Si no lo haces, no sabes si el modelo aprendio o simplemente memorizo.

Errores comunes en fine-tuning

  • - Dataset contaminado:datos de eval en el train set. Resultados inflados. Separar antes de cualquier preprocessing.
  • Overfitting:loss baja a cero, pero el modelo solo sabe repetir los ejemplos de entrenamiento. Solucion: menos epochs, mas dropout, o mas datos.
  • Catastrophic forgetting:el modelo pierde capacidades generales tras el fine-tuning. Solucion: usar LoRA (preserva pesos base), reducir learning rate, o mezclar datos generales con datos especificos.
  • Formato inconsistente:el dataset mezcla formatos de salida. El modelo no sabe que formato producir. Solucion: normalizar todos los outputs al mismo formato antes de entrenar.
  • Datos de baja calidad:"garbage in, garbage out" aplica especialmente a fine-tuning. 500 ejemplos perfectos superan a 5000 ejemplos mediocres.

Siguiente: TE05 - MLOps and CI/CD for AI

Your model is trained and serving. Now you need to manage it like professional software: versioning, registry, testing, automated deployments and rollback.

Ir al modulo TE05