Saturday, August 8, 2026
HomeArtificial IntelligenceSmall Language Fashions with Hugging Face transformers Library + smolLM3

Small Language Fashions with Hugging Face transformers Library + smolLM3

Small Language Fashions with Hugging Face transformers Library + smolLM3
 

Small However Highly effective

 
Working a 70B mannequin in manufacturing may be costly, sluggish, and, for a lot of duties, pointless. In the event you’re constructing a targeted pipeline like a doc classifier or a multilingual help responder, a well-trained 3B mannequin will match or beat the 70B in your particular activity at a fraction of the fee. The 3B mannequin suits solely in a single client GPU. It hundreds in seconds. It prices nothing per token. And on constrained {hardware}, it is the one choice 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 probably the most technically fascinating SLM obtainable on the 3B scale proper now, skilled 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 complete coaching blueprint printed alongside the weights.

The venture thread woven via each part: a multilingual buyer help 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 top, 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, information high quality, coaching curriculum, and architectural decisions matter extra.

Analysis from the SmolLM2 paper (arxiv, February 2025) confirmed that on the 1B—3B scale, fastidiously curated coaching information constantly outperforms naively scaling parameters. SmolLM3 takes that additional: 11.2 trillion coaching tokens throughout a staged curriculum — net, code, math, and reasoning information — 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 International MMLU (multilingual QA), it scores 53.5 in opposition to Llama-3.1-3B’s 46.8.

The place SLMs genuinely fall quick: duties requiring deep, broad world data, aggressive trivia, complicated multi-hop reasoning over huge data graphs, and really long-form artistic writing with wealthy historic context. For these, you need the large mannequin. For every thing targeted and domain-specific, the SLM with fine-tuning in your information 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 correct alternative for many new initiatives 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 customary. Three architectural selections inside that customary body are much less widespread and price understanding as a result of they instantly have an effect on the way you deploy and tune the mannequin.

  1. Grouped Question Consideration: Commonplace 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, decreasing 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 implies 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: suppose and no_think. In suppose 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 through the system immediate or the enable_thinking kwarg within the chat template. No additional mannequin, no additional checkpoint.

 

Setting Up Your Setting

 
{Hardware} minimums:

 

Characteristic 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 technology duties relying in your machine. Advantageous-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 surroundings
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"

 

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

 

System detection helper (run this primary):

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

def detect_device():
    """
    Detect the very best obtainable compute gadget.
    Returns (device_str, dtype_str, load_kwargs) to be used with from_pretrained.
    """
    strive:
        import torch
    besides ImportError:
        increase 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 really useful 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 -- operating on CPU (slower however purposeful)")
        return "cpu", torch.float32, {"device_map": "cpu", "torch_dtype": torch.float32}


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

 

How one can run:

 

Anticipated output (NVIDIA GPU instance):

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

 

Loading SmolLM3 and Working Your First Inference

 
With the surroundings confirmed, here is the whole 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 obtainable GPUs, or CPU if none
)
mannequin.eval()

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

# ── 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 mechanically
    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.gadget)

    with torch.no_grad():
        output_ids = mannequin.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=0.6,   # Really helpful by the SmolLM3 group 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 suppose mode the mannequin prefixes its response with ....
    # Callers normally solely want the ultimate reply that follows.
    last = re.sub(r".*?", "", uncooked, flags=re.DOTALL).strip()
    return last


# ── 3. Examine suppose vs no_think on the identical immediate ──────────────────────────

immediate = "A buyer is charged twice for a similar order. What are three concrete steps help 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},
]

# suppose: reasoning hint earlier than reply -- good for complicated selections 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── suppose mode ──")
print(generate(think_messages, max_new_tokens=512))

 

How one can 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 a couple of seconds.

Once you evaluate the 2 outputs, suppose mode produces a noticeably extra structured reply; it causes via the steps earlier than committing. no_think is quicker and sometimes enough for routine duties. The proper mode is determined by your latency funds and activity complexity. For the ticket router venture coming subsequent, we’ll use no_think for classification (latency-sensitive) and suppose for escalation selections (accuracy-sensitive).

 

Constructing a Multilingual Help Ticket Router

 
Now the core venture. The TicketRouter class takes a help 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 evaluate.

It is a sample used at scale in actual help operations. The SmolLM3 model runs solely offline, with no API key, no information leaving the server, and no per-ticket price. That issues for any help 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

# ── Knowledge class for a routing consequence ──────────────────────────────────────────

@dataclass
class RoutingResult:
    ticket: str
    class: str           # billing | technical | account | basic
    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 help router for a SaaS firm.
Your job is to categorise help tickets and draft a useful, skilled reply.

Guidelines:
- Detect the language of the ticket mechanically.
- Classify into EXACTLY ONE of: billing, technical, account, basic.
- 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 outdoors your data.
- Reply ONLY with a single JSON object -- no preamble, no clarification outdoors 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.gadget}")

    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.gadget)

        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 'basic' 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": "basic", "confidence": 0.0, "reply": uncooked}
        strive:
            return json.hundreds(match.group())
        besides json.JSONDecodeError:
            return {"class": "basic", "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", "basic")
        confidence = float(parsed.get("confidence", 0.0))
        reply      = parsed.get("reply", "Thanks for reaching out. We are going to 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 ends in 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)}")

 

How one can 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, quick messages, mixed-language content material, and requests that might match two classes reliably produce decrease confidence scores. That is the sign you need: the mannequin being trustworthy about uncertainty moderately than guessing confidently and propagating a mistaken classification downstream.

 

Including Instrument Calling to SmolLM3

 
The ticket router works effectively for classification and reply technology. 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 help” — neither of which is helpful.

SmolLM3 helps device calling natively. You outline a device as a JSON Schema, cross it through 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 true operate, inject the consequence, and let the mannequin generate the ultimate response.

Here is the complete 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()

# ── Instrument definition ───────────────────────────────────────────────────────────
# SmolLM3 accepts device definitions as JSON Schema objects underneath xml_tools.
# The mannequin makes use of the title and outline to determine 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, change this with an actual database or API name.
    Returns a dict the mannequin can learn and summarize for the shopper.
    """
    database = {
        "ORD-4821": {"standing": "shipped",    "eta": "June 18, 2026", "provider": "DHL"},
        "ORD-3307": {"standing": "processing", "eta": "June 20, 2026", "provider": None},
        "ORD-1190": {"standing": "delivered",  "eta": None,            "provider": "FedEx"},
    }
    return database.get(order_id, {"standing": "not_found", "eta": None, "provider": None})

# ── Instrument 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
    strive:
        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 consequence.
    3. Generate the ultimate customer-facing response.
    """
    # Flip 1: give the mannequin the person message and obtainable instruments
    messages = [{"role": "user", "content": user_message}]

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

    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 true operate
        tool_result = lookup_order_status(**tool_args)
        print(f"  [Tool called] {tool_name}({tool_args}) → {tool_result}")

        # Flip 2: inject the device consequence 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.gadget)

        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()


# ── Take a look at 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}")

 

How one can run:

 

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

 

Advantageous-Tuning SmolLM3 on Area Knowledge

 
A 3B mannequin is sufficiently small to fine-tune on a single client GPU in minutes, not hours. The result’s a mannequin that is aware of your area vocabulary, your response fashion, 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 implies we’re coaching solely a small fraction of parameters — usually underneath 1% — and merging the adapter again into the bottom mannequin on the finish.

# finetune.py
# Further conditions: 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 help router for a SaaS firm.
Classify the help 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 information ─────────────────────────────────────────────────────────────
# In manufacturing you'd load tons of of actual labelled tickets.
# This minimal set demonstrates the format -- increase along with your actual information.

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 accommodates the goal JSON the mannequin ought to be taught to provide.
    """
    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 probably the most
# task-specific sign and provides the very best 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 quick -- no want for the complete context window
)

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

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

# ── 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')")

 

How one can 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}

 

Advantageous-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 information (tons of 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 operating your domain-tuned router.

 

Conclusion

 
SmolLM3 makes the case that parameter rely is just not the first metric. A 3B mannequin skilled on 11.2 trillion tokens with the correct architectural decisions — 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 options.

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

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

Assets:

 
 

Shittu Olumide is a software program engineer and technical author keen about leveraging cutting-edge applied sciences to craft compelling narratives, with a eager eye for element and a knack for simplifying complicated ideas. You may as well discover Shittu on Twitter.


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments