Friday, August 7, 2026

Small Language Fashions with Hugging Face transformers Library + smolLM3


 

Small However Highly effective

 
Working a 70B mannequin in manufacturing might be costly, sluggish, and, for a lot of duties, pointless. For those who’re constructing a targeted pipeline like a doc classifier or a multilingual assist responder, a well-trained 3B mannequin will match or beat the 70B in your particular process at a fraction of the price. The 3B mannequin matches completely in a single shopper GPU. It hundreds in seconds. It prices nothing per token. And on constrained {hardware}, it is the one possibility that runs in any respect.

That is the precise case for small language fashions (SLMs). This text makes use of SmolLM3, Hugging Face’s flagship 3B mannequin launched on July 8, 2025, because the working mannequin all through. It is essentially the most technically attention-grabbing SLM out there on the 3B scale proper now, educated on 11.2 trillion tokens, supporting a 128k context window, dual-mode reasoning, native device calling, six languages, and an Apache 2.0 license with the total coaching blueprint revealed alongside the weights.

The challenge thread woven by way of each part: a multilingual buyer assist ticket router that classifies incoming tickets by class, detects the ticket language, generates a reply in that very same language, and flags low-confidence outputs for human escalation. By the tip, you may have a working pipeline you possibly can adapt to your individual area.

 

Why Small Language Fashions Deserve Extra Consideration

 
The parameter-count fixation in AI is comprehensible however deceptive. Uncooked scale issues, up to some extent. After that time, knowledge high quality, coaching curriculum, and architectural selections matter extra.

Analysis from the SmolLM2 paper (arxiv, February 2025) confirmed that on the 1B—3B scale, rigorously curated coaching knowledge persistently outperforms naively scaling parameters. SmolLM3 takes that additional: 11.2 trillion coaching tokens throughout a staged curriculum — net, code, math, and reasoning knowledge — plus 140 billion reasoning tokens in post-training. The result’s a mannequin that, on zero-shot benchmarks, outperforms each Llama-3.2-3B and Qwen2.5-3B and rivals Qwen3-4B on a number of duties.

Take the IFEval instruction-following benchmark, the place SmolLM3 scores 76.7, larger than Qwen3-4B at 68.9. On BFCL (device calling), it ties Llama’s tool-call fine-tune at 92.3. On World MMLU (multilingual QA), it scores 53.5 towards Llama-3.1-3B’s 46.8.

The place SLMs genuinely fall brief: duties requiring deep, broad world information, aggressive trivia, advanced multi-hop reasoning over huge information graphs, and really long-form artistic writing with wealthy historic context. For these, you need the massive mannequin. For the whole lot targeted and domain-specific, the SLM with fine-tuning in your knowledge will match it at a tenth of the working price.

The Hugging Face SLM assortment at the moment contains SmolLM3-3B (instruction-tuned, what this text makes use of), SmolLM3-3B-Base (untuned pretrained weights), SmolLM2-1.7B (lighter predecessor), and SmolVLM (the vision-language variant). SmolLM3 is the fitting selection for many new tasks as a result of dual-mode reasoning, device calling, and the 128k context window are uncommon at this parameter scale.

 

Understanding SmolLM3’s Structure

 
SmolLM3 is a decoder-only transformer, which is normal. Three architectural choices inside that normal body are much less frequent and value understanding as a result of they instantly have an effect on the way you deploy and tune the mannequin.

  1. Grouped Question Consideration: Customary multi-head consideration maintains separate key and worth projections for every of the 16 consideration heads. SmolLM3 teams these 16 heads into 4 shared question projections, lowering key-value (KV) cache reminiscence by roughly 25% with out measurable accuracy loss. This issues at inference time: a smaller KV cache means decrease peak VRAM, which suggests you possibly can course of longer contexts or bigger batches on the identical {hardware}.
  2. NoPE (No Positional Encoding on choose layers): SmolLM3 removes rotary positional encoding (RoPE) from each fourth transformer layer, implementing a 3:1 RoPE-to-NoPE ratio. This strategy comes from the 2025 paper “RoPE to NoRoPE and Again Once more” and helps the mannequin generalize over lengthy contexts with out the positional embedding degradation that impacts most different small fashions at lengthy sequence lengths.
  3. Twin-mode reasoning: A single set of weights handles two modes: assume and no_think. In assume mode, the mannequin generates a chain-of-thought hint inside ... tags earlier than the ultimate reply, equal to what separate “reasoning fashions” do. In no_think mode, it solutions instantly. You management this per-request by way of the system immediate or the enable_thinking kwarg within the chat template. No further mannequin, no further checkpoint.

 

Setting Up Your Surroundings

 
{Hardware} minimums:

 

Function Minimal Really helpful
GPU VRAM 6 GB (bfloat16) 8 GB+ (RTX 3060 or higher)
System RAM 16 GB 32 GB
Disk 8 GB free 20 GB+ SSD
Apple Silicon M2 8 GB M2 Professional / M3 16 GB

 

CPU-only works. Anticipate roughly 3x slower inference for text-to-speech (TTS) synthesis and 5—8 tokens/second on era duties relying in your machine. Positive-tuning on CPU is impractical; use Google Colab’s free T4 GPU if you do not have an area GPU.

Python and packages:

# Python 3.10 or newer required
python --version

# Create and activate a digital atmosphere
python -m venv smollm-env
supply smollm-env/bin/activate       # macOS / Linux
smollm-envScriptsactivate          # Home windows

# Set up all dependencies
pip set up 
  "transformers>=4.53.0" 
  "torch>=2.3.0" 
  "speed up>=0.30.0" 
  "bitsandbytes>=0.43.0" 
  "sentencepiece" 
  "trl>=0.9.0" 
  "peft>=0.11.0" 
  "datasets>=2.19.0"

 

Observe: transformers>=4.53.0 is required; SmolLM3’s modeling code shipped in that launch. Earlier variations will fail with an unrecognized structure error.

 

Machine detection helper (run this primary):

# device_check.py
# Run this earlier than the rest to substantiate your setup and choose the fitting dtype.

def detect_device():
    """
    Detect the perfect out there compute machine.
    Returns (device_str, dtype_str, load_kwargs) to be used with from_pretrained.
    """
    attempt:
        import torch
    besides ImportError:
        elevate RuntimeError("PyTorch not discovered. Set up with: pip set up torch")

    if torch.cuda.is_available():
        vram_gb = torch.cuda.get_device_properties(0).total_memory / 1e9
        print(f"CUDA GPU detected: {torch.cuda.get_device_name(0)} ({vram_gb:.1f} GB VRAM)")
        # bfloat16 is advisable for SmolLM3 -- it is the coaching dtype
        return "cuda", torch.bfloat16, {"device_map": "auto", "torch_dtype": torch.bfloat16}

    elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
        print("Apple Silicon MPS detected")
        # MPS helps float16 however not all bfloat16 ops -- use float16 on Apple Silicon
        return "mps", torch.float16, {"device_map": "mps", "torch_dtype": torch.float16}

    else:
        print("No GPU discovered -- working on CPU (slower however useful)")
        return "cpu", torch.float32, {"device_map": "cpu", "torch_dtype": torch.float32}


if __name__ == "__main__":
    machine, dtype, kwargs = detect_device()
    print(f"Machine : {machine}")
    print(f"Dtype  : {dtype}")
    print(f"Kwargs : {kwargs}")

 

Methods to run:

 

Anticipated output (NVIDIA GPU instance):

CUDA GPU detected: NVIDIA GeForce RTX 3060 (12.0 GB VRAM)
Machine : cuda
Dtype  : torch.bfloat16
Kwargs : {'device_map': 'auto', 'torch_dtype': torch.bfloat16}

 

Loading SmolLM3 and Working Your First Inference

 
With the atmosphere confirmed, here is the entire load-and-generate sample. This covers dtype choice, device_map="auto" for multi-GPU or CPU offload, and each pondering modes aspect by aspect.

# first_inference.py
# Conditions: transformers>=4.53.0, torch, speed up
# Run: python first_inference.py

import re
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID = "HuggingFaceTB/SmolLM3-3B"

# ── 1. Load tokenizer and mannequin ───────────────────────────────────────────────

print(f"Loading {MODEL_ID}...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

mannequin = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,    # Match the coaching dtype; use float16 on Apple Silicon
    device_map="auto",             # Spreads throughout all out there GPUs, or CPU if none
)
mannequin.eval()

print(f"Mannequin loaded on: {mannequin.machine}")

# ── 2. Technology helper ──────────────────────────────────────────────────────

def generate(messages: listing[dict], max_new_tokens: int = 512) -> str:
    """
    Apply the SmolLM3 chat template, tokenize, generate, and decode.
    Strips the ... block from the output routinely
    so callers all the time obtain the ultimate reply solely.
    """
    # apply_chat_template codecs messages utilizing SmolLM3's built-in chat template
    textual content = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )
    inputs = tokenizer(textual content, return_tensors="pt").to(mannequin.machine)

    with torch.no_grad():
        output_ids = mannequin.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=0.6,   # Really helpful by the SmolLM3 crew for balanced output
            top_p=0.95,        # Nucleus sampling -- retains output targeted with out being repetitive
            do_sample=True,
        )

    # Decode solely the newly generated tokens, not the enter immediate
    new_tokens = output_ids[0][inputs["input_ids"].form[-1]:]
    uncooked = tokenizer.decode(new_tokens, skip_special_tokens=True)

    # Strip the chain-of-thought block if current.
    # In assume mode the mannequin prefixes its response with ....
    # Callers often solely want the ultimate reply that follows.
    last = re.sub(r".*?", "", uncooked, flags=re.DOTALL).strip()
    return last


# ── 3. Evaluate assume vs no_think on the identical immediate ──────────────────────────

immediate = "A buyer is charged twice for a similar order. What are three concrete steps assist ought to take?"

# no_think: quick, direct reply -- good for high-throughput classification and replies
no_think_messages = [
    {"role": "system", "content": "/no_think"},
    {"role": "user",   "content": prompt},
]

# assume: reasoning hint earlier than reply -- good for advanced choices and edge circumstances
think_messages = [
    {"role": "system", "content": "/think"},
    {"role": "user",   "content": prompt},
]

print("n── no_think mode ──")
print(generate(no_think_messages, max_new_tokens=256))

print("n── assume mode ──")
print(generate(think_messages, max_new_tokens=512))

 

Methods to run:

python first_inference.py

 

The mannequin downloads to ~/.cache/huggingface/hub/ on first run (~6.7 GB). On subsequent runs, it hundreds from cache in just a few seconds.

Whenever you evaluate the 2 outputs, assume mode produces a noticeably extra structured reply; it causes by way of the steps earlier than committing. no_think is quicker and infrequently adequate for routine duties. The appropriate mode depends upon your latency finances and process complexity. For the ticket router challenge coming subsequent, we’ll use no_think for classification (latency-sensitive) and assume for escalation choices (accuracy-sensitive).

 

Constructing a Multilingual Help Ticket Router

 
Now the core challenge. The TicketRouter class takes a assist ticket in any of SmolLM3’s six natively supported languages (English, French, Spanish, German, Italian, Portuguese), classifies it right into a class, generates a reply within the ticket’s personal language, and flags low-confidence outputs for human overview.

This can be a sample used at scale in actual assist operations. The SmolLM3 model runs completely offline, with no API key, no knowledge leaving the server, and no per-ticket price. That issues for any assist system dealing with personally identifiable data (PII).

# ticket_router.py
# Conditions: transformers>=4.53.0, torch, speed up
# Run: python ticket_router.py

import re
import json
import torch
from dataclasses import dataclass
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID      = "HuggingFaceTB/SmolLM3-3B"
ESCALATE_AT   = 0.70   # Tickets with confidence under this go to a human agent

# ── Information class for a routing outcome ──────────────────────────────────────────

@dataclass
class RoutingResult:
    ticket: str
    class: str           # billing | technical | account | common
    confidence: float       # 0.0-1.0 self-reported by the mannequin
    reply: str              # Generated in the identical language because the ticket
    escalate: bool          # True when confidence < ESCALATE_AT
    raw_output: str         # Full mannequin output for debugging


# ── System immediate ─────────────────────────────────────────────────────────────

SYSTEM_PROMPT = """You're a multilingual buyer assist router for a SaaS firm.
Your job is to categorise assist tickets and draft a useful, skilled reply.

Guidelines:
- Detect the language of the ticket routinely.
- Classify into EXACTLY ONE of: billing, technical, account, common.
- Reply within the SAME language because the ticket.
- Fee your confidence actually from 0.0 to 1.0. Low confidence means the ticket is ambiguous or exterior your information.
- Reply ONLY with a single JSON object -- no preamble, no clarification exterior the JSON.

Required format:
{"class": "", "confidence": <0.0-1.0>, "reply": ""}"""


# ── Router class ──────────────────────────────────────────────────────────────

class TicketRouter:
    def __init__(self, model_id: str = MODEL_ID):
        print(f"Loading {model_id}...")
        self.tokenizer = AutoTokenizer.from_pretrained(model_id)
        self.mannequin = AutoModelForCausalLM.from_pretrained(
            model_id,
            torch_dtype=torch.bfloat16,
            device_map="auto",
        )
        self.mannequin.eval()
        print(f"Prepared on {self.mannequin.machine}")

    def _call_model(self, ticket: str) -> str:
        """
        Format the ticket right into a chat message, run inference in no_think mode
        (sooner for classification), and return the uncooked decoded output.
        """
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user",   "content": ticket},
        ]
        textual content = self.tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False,   # Quick path -- no chain-of-thought for routine classification
        )
        inputs = self.tokenizer(textual content, return_tensors="pt").to(self.mannequin.machine)

        with torch.no_grad():
            output_ids = self.mannequin.generate(
                **inputs,
                max_new_tokens=256,
                temperature=0.3,   # Decrease temp for classification -- extra deterministic output
                top_p=0.9,
                do_sample=True,
            )

        new_tokens = output_ids[0][inputs["input_ids"].form[-1]:]
        return self.tokenizer.decode(new_tokens, skip_special_tokens=True).strip()

    def _parse_output(self, uncooked: str) -> dict:
        """
        Extract the JSON object from the mannequin's output.
        Falls again to a default 'common' class with zero confidence if parsing fails.
        This prevents a JSON parse failure from crashing the pipeline.
        """
        # Discover any JSON object within the output, even when surrounded by stray textual content
        match = re.search(r"{.*?}", uncooked, re.DOTALL)
        if not match:
            return {"class": "common", "confidence": 0.0, "reply": uncooked}
        attempt:
            return json.hundreds(match.group())
        besides json.JSONDecodeError:
            return {"class": "common", "confidence": 0.0, "reply": uncooked}

    def route(self, ticket: str) -> RoutingResult:
        """
        Route a single ticket. Returns a RoutingResult with classification,
        confidence, reply, and escalation flag.
        """
        uncooked = self._call_model(ticket)
        parsed = self._parse_output(uncooked)

        class   = parsed.get("class", "common")
        confidence = float(parsed.get("confidence", 0.0))
        reply      = parsed.get("reply", "Thanks for reaching out. We'll comply with up shortly.")

        return RoutingResult(
            ticket=ticket,
            class=class,
            confidence=confidence,
            reply=reply,
            escalate=confidence < ESCALATE_AT,
            raw_output=uncooked,
        )

    def route_batch(self, tickets: listing[str]) -> listing[RoutingResult]:
        """Route an inventory of tickets sequentially. Returns leads to enter order."""
        return [self.route(t) for t in tickets]


# ── Run it ────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    router = TicketRouter()

    test_tickets = [
        "I was charged twice for my subscription this month. Please refund the duplicate charge.",
        "L'application se bloque chaque fois que j'essaie d'exporter un fichier PDF.",   # French
        "No puedo iniciar sesión en mi cuenta desde hace dos días.",                     # Spanish
        "Die Rechnung für März fehlt in meinem Abrechnungsbereich.",                     # German
        "Il mio abbonamento non si rinnova automaticamente nonostante il pagamento.",    # Italian
    ]

    print("n" + "=" * 70)
    outcomes = router.route_batch(test_tickets)

    for r in outcomes:
        flag = "🔴 ESCALATE" if r.escalate else "🟢 AUTO"
        print(f"n{flag}")
        print(f"Ticket     : {r.ticket[:70]}...")
        print(f"Class   : {r.class}")
        print(f"Confidence : {r.confidence:.2f}")
        print(f"Reply      : {r.reply[:100]}...")

    escalated = [r for r in results if r.escalate]
    print(f"n{'─'*70}")
    print(f"Complete tickets : {len(outcomes)}")
    print(f"Auto-routed   : {len(outcomes) - len(escalated)}")
    print(f"Escalated     : {len(escalated)}")

 

Methods to run:

 

What to search for within the output: tickets the place the mannequin returns a confidence under 0.70 will likely be flagged for escalation. Ambiguous tickets, brief messages, mixed-language content material, and requests that would match two classes reliably produce decrease confidence scores. That is the sign you need: the mannequin being sincere about uncertainty reasonably than guessing confidently and propagating a flawed classification downstream.

 

Including Software Calling to SmolLM3

 
The ticket router works effectively for classification and reply era. However what occurs when a buyer asks a couple of particular order? The mannequin would not have entry to your database. With out device calling, it both hallucinates a solution or deflects with “please contact assist” — neither of which is helpful.

SmolLM3 helps device calling natively. You outline a device as a JSON Schema, cross it by way of xml_tools within the chat template, and the mannequin emits a structured block when it decides the device is required. You parse that block, name the actual operate, inject the outcome, and let the mannequin generate the ultimate response.

This is the total round-trip for an order lookup:

# tool_calling.py
# Conditions: transformers>=4.53.0, torch, speed up
# Run: python tool_calling.py

import re
import json
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID  = "HuggingFaceTB/SmolLM3-3B"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
mannequin     = AutoModelForCausalLM.from_pretrained(
    MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto"
)
mannequin.eval()

# ── Software definition ───────────────────────────────────────────────────────────
# SmolLM3 accepts device definitions as JSON Schema objects beneath xml_tools.
# The mannequin makes use of the title and outline to resolve when to name the device.
# The parameters schema tells it what arguments to incorporate within the name.

TOOLS = [
    {
        "name": "lookup_order_status",
        "description": (
            "Look up the current status, estimated delivery date, and carrier "
            "for a specific customer order. Call this when the customer mentions "
            "an order number or asks where their order is."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "description": "The order ID, usually in the format ORD-XXXXXX."
                }
            },
            "required": ["order_id"]
        }
    }
]

# ── Simulated order database ──────────────────────────────────────────────────

def lookup_order_status(order_id: str) -> dict:
    """
    In manufacturing, substitute this with an actual database or API name.
    Returns a dict the mannequin can learn and summarize for the client.
    """
    database = {
        "ORD-4821": {"standing": "shipped",    "eta": "June 18, 2026", "service": "DHL"},
        "ORD-3307": {"standing": "processing", "eta": "June 20, 2026", "service": None},
        "ORD-1190": {"standing": "delivered",  "eta": None,            "service": "FedEx"},
    }
    return database.get(order_id, {"standing": "not_found", "eta": None, "service": None})

# ── Software name parser ──────────────────────────────────────────────────────────

def parse_tool_call(output: str):
    """
    Extract a device name from the mannequin's output.
    SmolLM3 emits: {"title": "...", "arguments": {...}}
    Returns (tool_name, arguments) or (None, None) if no device name is current.
    """
    match = re.search(r"(.*?)", output, re.DOTALL)
    if not match:
        return None, None
    attempt:
        payload = json.hundreds(match.group(1).strip())
        return payload.get("title"), payload.get("arguments", {})
    besides json.JSONDecodeError:
        return None, None

# ── Full tool-call spherical journey ─────────────────────────────────────────────────

def respond_with_tools(user_message: str) -> str:
    """
    Full agentic loop:
    1. Ship person message + device definitions to the mannequin.
    2. If the mannequin emits a device name, execute it and inject the outcome.
    3. Generate the ultimate customer-facing response.
    """
    # Flip 1: give the mannequin the person message and out there instruments
    messages = [{"role": "user", "content": user_message}]

    inputs = tokenizer.apply_chat_template(
        messages,
        xml_tools=TOOLS,           # Move device definitions right here
        enable_thinking=False,
        add_generation_prompt=True,
        tokenize=True,
        return_tensors="pt",
    ).to(mannequin.machine)

    with torch.no_grad():
        output_ids = mannequin.generate(
            inputs, max_new_tokens=256, temperature=0.3, top_p=0.9, do_sample=True
        )
    turn1 = tokenizer.decode(
        output_ids[0][inputs.shape[-1]:], skip_special_tokens=True
    )

    # Examine if the mannequin needs to name a device
    tool_name, tool_args = parse_tool_call(turn1)

    if tool_name == "lookup_order_status":
        # Execute the actual operate
        tool_result = lookup_order_status(**tool_args)
        print(f"  [Tool called] {tool_name}({tool_args}) → {tool_result}")

        # Flip 2: inject the device outcome and ask for the ultimate response
        messages += [
            {"role": "assistant", "content": turn1},
            {"role": "tool",      "content": json.dumps(tool_result), "name": tool_name},
        ]
        inputs2 = tokenizer.apply_chat_template(
            messages,
            xml_tools=TOOLS,
            enable_thinking=False,
            add_generation_prompt=True,
            tokenize=True,
            return_tensors="pt",
        ).to(mannequin.machine)

        with torch.no_grad():
            output_ids2 = mannequin.generate(
                inputs2, max_new_tokens=256, temperature=0.3, top_p=0.9, do_sample=True
            )
        return tokenizer.decode(
            output_ids2[0][inputs2.shape[-1]:], skip_special_tokens=True
        ).strip()

    # No device name -- mannequin answered instantly
    return turn1.strip()


# ── Check it ───────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    queries = [
        "Where is my order ORD-4821? It's been a week.",
        "My order ORD-3307 hasn't shipped yet -- what's the status?",
        "I just want to change my email address.",  # No tool needed
    ]

    for question in queries:
        print(f"nCustomer : {question}")
        response = respond_with_tools(question)
        print(f"Agent    : {response}")

 

Methods to run:

 

The mannequin routes order-related queries by way of the lookup_order_status device and generates the ultimate reply utilizing the actual database outcome. For the email-change question, it solutions instantly with out calling any device. That selective invocation — calling instruments solely once they’re wanted — is what makes the agentic sample sensible.

 

Positive-Tuning SmolLM3 on Area Information

 
A 3B mannequin is sufficiently small to fine-tune on a single shopper GPU in minutes, not hours. The result’s a mannequin that is aware of your area vocabulary, your response type, and your escalation logic, as an alternative of counting on immediate engineering to approximate it at each inference name.

This part makes use of the TRL library’s SFTTrainer with LoRA adapters from PEFT, which suggests we’re coaching solely a small fraction of parameters — sometimes beneath 1% — and merging the adapter again into the bottom mannequin on the finish.

# finetune.py
# Extra stipulations: pip set up trl>=0.9.0 peft>=0.11.0 datasets>=2.19.0
# Run: python finetune.py
# Time: ~8-12 minutes on an RTX 3060 for 3 epochs over 50 examples

import json
import torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig

MODEL_ID   = "HuggingFaceTB/SmolLM3-3B"
OUTPUT_DIR = "./smollm3-ticket-router"

# ── System immediate (similar because the inference router) ──────────────────────────────

SYSTEM_PROMPT = """You're a multilingual buyer assist router for a SaaS firm.
Classify the assist ticket and generate a useful reply in the identical language because the ticket.
Reply ONLY with JSON: {"class": "", "confidence": <0.0-1.0>, "reply": ""}"""

# ── Coaching knowledge ─────────────────────────────────────────────────────────────
# In manufacturing you'd load a whole lot of actual labelled tickets.
# This minimal set demonstrates the format -- broaden together with your actual knowledge.

raw_examples = [
    ("I was charged twice for my subscription.", "billing",
     "We're sorry for the duplicate charge. Our billing team will review and issue a refund within 3-5 business days."),
    ("The app crashes every time I try to export a PDF.", "technical",
     "We apologize for the inconvenience. Our engineering team has been notified and will investigate."),
    ("I can't log into my account since yesterday.", "account",
     "We're sorry you're having trouble. Please try resetting your password. If the issue continues, we'll escalate to our account team."),
    ("Die App stürzt beim Exportieren von PDFs ab.", "technical",
     "Wir entschuldigen uns für die Unannehmlichkeiten. Unser Technikteam wurde benachrichtigt und untersucht das Problem."),
    ("L'application se bloque quand j'exporte un fichier.", "technical",
     "Nous nous excusons pour la gêne occasionnée. Notre équipe technique a été informée et travaille sur ce problème."),
    ("My March invoice is missing from the billing section.", "billing",
     "Thank you for flagging this. Our billing team will locate your March invoice and resend it within 24 hours."),
    ("No puedo iniciar sesión desde ayer por la noche.", "account",
     "Lamentamos el problema de acceso. Por favor, restablezca su contraseña. Si el problema persiste, escalaremos su caso."),
    ("How do I upgrade my plan to the Pro tier?", "general",
     "You can upgrade to Pro directly from Settings → Subscription. The new rate applies from your next billing cycle."),
]

def format_example(ticket: str, class: str, reply: str) -> dict:
    """
    Format a single instance into the SmolLM3 messages format.
    The assistant flip comprises the goal JSON the mannequin ought to be taught to supply.
    """
    return {
        "messages": [
            {"role": "system",    "content": SYSTEM_PROMPT},
            {"role": "user",      "content": ticket},
            {"role": "assistant", "content": json.dumps({
                "category": category, "confidence": 0.95, "reply": reply
            })},
        ]
    }

dataset = Dataset.from_list([format_example(*ex) for ex in raw_examples])

# ── Tokenizer ─────────────────────────────────────────────────────────────────

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.pad_token = tokenizer.eos_token   # SmolLM3 has no separate pad token

# ── Mannequin (4-bit quantized base for QLoRA) ────────────────────────────────────

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
)
mannequin = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=bnb_config,
    device_map="auto",
)

# ── LoRA config ───────────────────────────────────────────────────────────────
# We goal the eye and MLP projection layers -- these carry essentially the most
# task-specific sign and provides the perfect accuracy/parameter trade-off.

lora_config = LoraConfig(
    r=16,              # Rank of the LoRA replace matrices -- larger = extra expressive, extra reminiscence
    lora_alpha=32,     # Scaling issue; conventionally set to 2*r
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",   # Attention projections
        "gate_proj", "up_proj", "down_proj",        # MLP projections (SwiGLU)
    ],
)
mannequin = get_peft_model(mannequin, lora_config)
mannequin.print_trainable_parameters()
# Anticipated: trainable params: ~13M (0.4% of 3B whole)

# ── Coaching config ───────────────────────────────────────────────────────────

sft_config = SFTConfig(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,   # Efficient batch dimension = 8
    learning_rate=2e-4,
    warmup_ratio=0.1,
    lr_scheduler_type="cosine",
    bf16=True,
    logging_steps=5,
    save_strategy="epoch",
    max_seq_length=512,              # Tickets are brief -- no want for the total context window
)

# ── Practice ─────────────────────────────────────────────────────────────────────

coach = SFTTrainer(
    mannequin=mannequin,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=sft_config,
)
coach.prepare()

# ── Save and merge ────────────────────────────────────────────────────────────
# Save the LoRA adapter -- small file, straightforward to share or model.
coach.save_model(f"{OUTPUT_DIR}/adapter")

# Merge the adapter again into the bottom mannequin weights for standalone deployment.
# The merged mannequin hundreds precisely like the bottom mannequin -- no PEFT dependency at inference.
merged = mannequin.merge_and_unload()
merged.save_pretrained(f"{OUTPUT_DIR}/merged")
tokenizer.save_pretrained(f"{OUTPUT_DIR}/merged")

print(f"nFine-tuned mannequin saved to {OUTPUT_DIR}/merged")
print("Load it with: AutoModelForCausalLM.from_pretrained('./smollm3-ticket-router/merged')")

 

Methods to run:

pip set up trl>=0.9.0 peft>=0.11.0 datasets>=2.19.0
python finetune.py

 

Anticipated coaching output:

trainable params: 13,631,488 || all params: 3,085,123,584 || trainable%: 0.4420
{'loss': 1.842, 'learning_rate': 2e-04, 'epoch': 0.5}
{'loss': 0.923, 'learning_rate': 1.4e-04, 'epoch': 1.0}
{'loss': 0.461, 'learning_rate': 6e-05, 'epoch': 2.0}
{'loss': 0.287, 'learning_rate': 0.0, 'epoch': 3.0}

 

Positive-tuned mannequin saved to ./smollm3-ticket-router/merged.

The loss dropping from 1.8 to 0.3 throughout three epochs tells you the mannequin is studying the duty format. On actual knowledge (a whole lot of examples throughout your particular classes), you may see the classification accuracy and reply high quality enhance noticeably in comparison with the bottom mannequin with immediate engineering alone.

After coaching, swap MODEL_ID in ticket_router.py for "./smollm3-ticket-router/merged" and also you’re working your domain-tuned router.

 

Conclusion

 
SmolLM3 makes the case that parameter rely isn’t the first metric. A 3B mannequin educated on 11.2 trillion tokens with the fitting architectural selections — grouped question consideration (GQA), NoPE, and dual-mode reasoning — delivers production-viable outcomes on targeted duties at a fraction of the latency, price, and {hardware} necessities of 70B alternate options.

The ticket router challenge on this article covers the total manufacturing sample: load as soon as, route many, escalate on low confidence, name instruments for dwell knowledge, fine-tune on area knowledge, and quantize for constrained {hardware}. Every of these strategies applies to any targeted pure language processing (NLP) process. Swap the ticket examples in your area, alter the class labels, and you’ve got a basis value deploying.

The SmolLM3 GitHub repo has the total coaching code, knowledge combination particulars, and analysis configs. The mannequin web page has the benchmark tables in full and the quantized mannequin assortment. The SmolLM3 weblog submit covers the coaching choices in depth if you wish to perceive the architectural selections earlier than constructing on prime of them.

Assets:

 
 

Shittu Olumide is a software program engineer and technical author enthusiastic about leveraging cutting-edge applied sciences to craft compelling narratives, with a eager eye for element and a knack for simplifying advanced ideas. You can too discover Shittu on Twitter.



Related Articles

Latest Articles