Go Faster: Unsloth and Config-as-Code (Axolotl) (Fine-Tuning in Production, Module 6)

Module 6 of 15 13 min read Lab code ↗

This is Module 6 of Fine-Tuning in Production, a free 15-module course that takes one open model from raw data to a deployed, published tool-calling specialist on the Hugging Face Hub. Start at Module 1 or browse the full syllabus.

You made it fit. Now you’re watching a progress bar.

Module 5 was a real win. You took Anvil — our Qwen3-4B-Base being hammered into a tool-calling specialist — and made it fit a free 16 GB T4 with QLoRA: a 4-bit NF4 base with LoRA adapters on top, the whole thing under 16 GB of VRAM. No rented GPU, no credit card, no OOM.

Then you ran it. And you waited. And you waited.

QLoRA pays for that 16 GB with time. Every matmul dequantizes 4-bit weights back to bf16 on the fly, and on a T4 that adds up — a few thousand steps over your tool-calling set turns into a coffee, then a lunch, then “I’ll check it after the meeting.” The obvious escape is to rent an A100 and throw money at the clock. But there’s a quieter question worth asking first: what if you could run the exact same QLoRA — same math, same artifact — roughly twice as fast and with less VRAM, on the same free T4, by changing how the model is loaded?

That’s the promise of Unsloth. And if you’d rather describe the whole run in one config file than in Python, that’s what Axolotl does. This module measures the real speedup (not the marketing number) and shows you both — while keeping a guardrail front and center: these are optional accelerators, not a dependency you’re forced to adopt.

In this module

You’ll learn how to:

  1. Replay the Module-5 QLoRA run through Unsloth with FastLanguageModel, and measure the real speedup and VRAM drop on your own T4 — not the marketing claim.
  2. Explain why Unsloth is an optional, drop-in accelerator (fused kernels, an optimized backward pass), not a different fine-tuning method and not a requirement — the math is the same QLoRA you already know.
  3. Read and run an equivalent Axolotl recipe: express Anvil’s QLoRA as a single declarative YAML (config-as-code) where one file drives the whole pipeline.
  4. Reason about vendor-neutrality: what you gain (speed, memory, a shareable config) and what you give up (a little lock-in, a little low-level control) by layering an accelerator on top of raw TRL/PEFT.
  5. Pin and date the tooling correctly — the PyPI date-style version (unsloth==2026.6.7) versus the diverging GitHub tags, and why a FastLanguageModel signature must be copied from a live notebook, never recited from memory.

You’ll build: Anvil’s QLoRA, faster. config.py gains a tier="unsloth" that loads the same Qwen3-4B QLoRA through Unsloth’s FastLanguageModel; you re-run the Module-5 training and measure the speedup and VRAM savings on the same free T4; and you add an equivalent anvil-qlora.yml Axolotl recipe that trains the same Anvil from one declarative file. tier="qlora" stays the default — these are accelerators you can take or leave.

Theory areas covered:

  • T6 — Accelerators & config-as-code (Unsloth, Axolotl) — 7% of the course. Module 6 owns this area; it’s reinforced later in Module 10 (Unsloth’s GRPO memory savings) and Module 15 (the capstone).

This module reinforces but does not re-teach two areas you already own: T5 (QLoRA) from Module 5 and T3 (SFT) from Module 3. We don’t re-explain QLoRA or supervised fine-tuning here — we replay them, faster, through a different front-end.

Prerequisites: Modules 1–5. Specifically: the installed stack (M1), the data.py chat-template layer (M2), the SFTTrainer/SFTConfig flow in train_sft.py (M3), the lora_config() / get_peft_model adapter setup (M4), and the tier="qlora" QLoRA path in config.py (M5). For the hands-on lab you need a free Colab/Kaggle T4 (16 GB) — the default path — or any local NVIDIA GPU with ≥ 16 GB. HF_TOKEN set in your environment (M1). One hard constraint: Unsloth needs a CUDA NVIDIA GPU (like bitsandbytes — no CPU or Apple Silicon for the real run); the offline smoke test stays on CPU and skips the accelerated path cleanly.

Where Anvil is now

From here on, every module marks where you are with (done), 👉 (you’re here), and (ahead):

  • M1–M5 — Anvil has a baseline, a curated tool-calling dataset, a full SFT run, LoRA adapters, and a QLoRA path that trains on a free T4.
  • 👉 M6 — Go Faster: Unsloth and Config-as-Code (Axolotl). Replay that QLoRA ~2× faster on the same T4; add an Axolotl YAML as an alternative front-end.
  • M7–M15 — evaluation, preferences (DPO), reasoning (GRPO), merging, quantization, serving, and publishing Anvil to the Hub.

Before this module: Anvil trains QLoRA on a free T4 — but slowly. After it: Anvil trains the same QLoRA noticeably faster on the same T4, with an Axolotl recipe as an alternative way to launch it. The accelerated path lives behind one new value — tier="unsloth" — inside config.py, the single place all model choices live. It’s an option: the default stays tier="qlora". That same tier="unsloth" is what makes Anvil’s GRPO run affordable on the free path in Module 10 (Unsloth claims “80% less VRAM for GRPO” — we’ll measure it there). It’s available in the capstone too. Never imposed.

The same QLoRA, faster: what Unsloth actually is

Start from where Module 5 left you. You have a QLoRA pipeline built from raw, standard parts: a BitsAndBytesConfig that loads the base in 4-bit NF4, prepare_model_for_kbit_training, a LoraConfig adapter, and TRL’s SFTTrainer. It works. It fits a T4. It’s slow.

Unsloth does not change the method. It’s still QLoRA: a frozen 4-bit NF4 base with bf16 LoRA adapters, trained with the same next-token SFT loss. What Unsloth changes is how the operations run. It ships hand-written fused Triton kernels, an optimized backward pass, patched attention and MLP paths, and tighter memory management — all aimed at doing the same arithmetic with fewer kernel launches, fewer intermediate tensors, and less wasted VRAM. The mental model to lock in: same math, more efficient execution.

The claim — stated, dated, and treated as a claim

Unsloth’s headline pitch, as of unsloth 2026.6.7 (June 2026), is “2× faster, up to 70% less VRAM” — and, for reinforcement learning, “80% less VRAM for GRPO” (that second one is the reason tier="unsloth" matters in Module 10). The project sits around 53k GitHub stars; read that as an order of magnitude, not a frozen number.

Here’s the course’s posture, and it’s the senior-engineer move: a vendor’s speed claim is a claim, not a constant. “2× faster” depends on the model, the GPU, the sequence length, the batch size, and the exact version of every library in the stack. So we don’t assert it — we measure it in the lab, report the number as “on my run,” and date it. If your T4 gives you 1.7× instead of 2.1×, nobody lied; you just measured your setup instead of theirs.

FastLanguageModel: the freshness trap of this module

Unsloth’s entry point is FastLanguageModel, with three calls that matter:

  • FastLanguageModel.from_pretrained(...) — loads the model and tokenizer in one shot, already quantized to 4-bit. Unsloth handles the QLoRA internally, so on this path you don’t construct a separate BitsAndBytesConfig.
  • FastLanguageModel.get_peft_model(...) — attaches the LoRA adapters (Unsloth’s counterpart to PEFT’s get_peft_model from Module 4).
  • FastLanguageModel.for_inference(...) — flips the model into fast-inference mode for generation after training (not central to the training lab).

⚠️ Common misconception: “I need Unsloth to fine-tune on a free GPU — and it changes how QLoRA works.”

Wrong on both counts. (1) Unsloth is an optional accelerator: the bare TRL/PEFT/bitsandbytes QLoRA from Module 5 already fits a free T4 — Unsloth just makes it faster. You are not blocked without it. (2) Unsloth does not change the math: it’s the same QLoRA (4-bit NF4 base + LoRA adapters, same SFT loss), executed with optimized kernels, and the output artifact is identical (a 4-bit base + a LoRA adapter you merge and quantize for serving in Module 12, unchanged). And Axolotl isn’t a new algorithm either — it’s a different interface (YAML) to the same training.

The genuinely sharp warning: never recite a FastLanguageModel signature from memory or an old tutorial. Its argument names and shape move between releases — Unsloth ships fast, and from_pretrained/get_peft_model arguments have changed historically. Half the Unsloth blog posts online use signatures that no longer load. When you write the tier="unsloth" branch, copy the exact signature from a current official Unsloth notebook for Qwen3-4B / QLoRA, and pin the version. The lab code does exactly this (and pins unsloth==2026.6.7).

Same recipe, two execution paths

The key picture: one QLoRA recipe, two ways to run it.

flowchart TB
    recipe["QLoRA recipe<br/>4-bit NF4 base + LoRA adapters<br/>SFT next-token loss"]
    recipe --> a["TRL / PEFT / bitsandbytes (M5)<br/>BitsAndBytesConfig → prepare_model_for_kbit_training<br/>→ get_peft_model → SFTTrainer"]
    recipe --> b["Unsloth (M6)<br/>FastLanguageModel.from_pretrained<br/>→ .get_peft_model → (the same) SFTTrainer"]
    a --> art["4-bit base + LoRA adapter<br/>(same kind of artifact)"]
    b --> art
    art --> tail["merge → quantize → serve (M12–M13)<br/>unchanged either way"]

Both paths produce the same kind of artifact — a 4-bit base plus a LoRA adapter — so everything downstream (merge, quantize, serve in Modules 12–13) is identical. Unsloth just runs the ops faster. And because Unsloth layers on top of the same Transformers/PEFT/TRL stack, you can plug it in and pull it out behind a tier flag. That’s exactly why it stays optional in this course.

Unsloth in config.py: a tier you can plug in and out

Anvil’s frozen contract puts every model decision in one place: get_model_and_tokenizer(tier), with tier ∈ {"full","qlora","unsloth","tiny"} (there’s no "lora" tier — the bf16 LoRA path from Module 4 is tier="full" with use_lora=True). Module 6 implements the "unsloth" branch. The whole point is that it returns the same (model, tokenizer) pair as every other tier, so the rest of the pipeline — train_sft.py, SFTTrainer, SFTConfig — runs unchanged.

Here’s the routing. The unsloth tier is dispatched separately at the top of the factory, because it uses a different loader API:

# anvil/config.py — the unsloth tier routes to its own loader (FastLanguageModel)
def get_model_and_tokenizer(tier: str = "qlora"):
    import torch
    from transformers import AutoModelForCausalLM, AutoTokenizer

    if tier == "unsloth":
        return _load_unsloth()  # different API (FastLanguageModel) — handled separately
    # ... "tiny" / "full" / "qlora" branches use AutoModelForCausalLM as before ...

And the loader itself — the one place Unsloth’s API appears. Note the comment: this signature is copied from a current notebook, not invented, and the version is pinned.

# anvil/config.py
def _load_unsloth():
    """Load the base via Unsloth's FastLanguageModel (M6 — optional accelerator, CUDA only)."""
    try:
        from unsloth import FastLanguageModel
    except ImportError as exc:
        raise RuntimeError(
            "tier='unsloth' needs the optional accelerator: `pip install "
            "'unsloth==2026.6.7'` (Unsloth, CUDA only). See Module 6."
        ) from exc
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name=MODEL_ID, max_seq_length=2048, load_in_4bit=True, dtype=None)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    return model, tokenizer

Two details earn their keep. First, that except ImportError is not a silent swallow — it re-raises a RuntimeError with an actionable message (“pip install 'unsloth==2026.6.7'”), because Unsloth isn’t installed by default and a reader on CPU should get a sentence, not a cryptic traceback. Second, max_seq_length=2048 here is an argument to Unsloth’s own from_pretrained — it is not the banned SFTConfig(max_seq_length=...). That distinction matters in the next paragraph.

config.py also ships an unsloth_lora() helper that attaches LoRA the Unsloth way — via FastLanguageModel.get_peft_model rather than peft.get_peft_model. It’s an available alternative, not the default path: Anvil’s train_sft(tier="unsloth", use_lora=True) loads the model with _load_unsloth() (which does not attach LoRA) and then lets SFTTrainer apply the adapter from peft_config=config.lora_config() — the same PEFT path (target_modules="all-linear") every other tier uses. So under the lab’s frozen flow, the Unsloth tier still applies LoRA through PEFT; unsloth_lora() is there if you’d rather drive Unsloth’s own LoRA call directly:

# anvil/config.py — an alternative helper; the default train_sft path does NOT call this
def unsloth_lora(model, r: int = 16, lora_alpha: int = 32):
    """Apply LoRA the Unsloth way (FastLanguageModel.get_peft_model) — not peft.get_peft_model.
    This is the one place the Unsloth path *can* diverge from the plain TRL/PEFT flow."""
    from unsloth import FastLanguageModel

    return FastLanguageModel.get_peft_model(
        model, r=r, lora_alpha=lora_alpha, lora_dropout=0.0,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                        "gate_proj", "up_proj", "down_proj"],
        use_gradient_checkpointing="unsloth", random_state=0)

Staying in TRL v1 idioms when a notebook tempts you not to

A live freshness trap lurks here. Many Unsloth notebooks still show old TRL idiomsSFTTrainer(..., tokenizer=tok) and SFTConfig(max_seq_length=...) — because they predate TRL’s v1 line. Do not regress to them just because you copied an Unsloth loader. Anvil’s train_sft.py already uses the current idioms, and the Unsloth model feeds the very same trainer:

# anvil/train_sft.py — TRL v1 idioms, unchanged whether tier is "qlora" or "unsloth"
trainer = SFTTrainer(
    model=model,
    args=args,                       # an SFTConfig (max_length, not max_seq_length)
    train_dataset=train_dataset,
    processing_class=tokenizer,      # TRL v1: processing_class, never tokenizer=
    peft_config=peft_config,
)

To be precise about the verified stack as of June 2026: we pin trl==1.6.0. TRL’s 1.x line has shipped on PyPI — “TRL v1” now is the major version, not just branding. The v1 idioms are unchanged and correct: processing_class (never tokenizer=), max_length (never max_seq_length on the config), and dataset/length/packing fields living on SFTConfig, not on the trainer. Unsloth brings its own compatible trl; the model it returns plugs into the standard SFTTrainer like any other (model, tokenizer).

What you gain, what you give up

This is the trade, line by line:

AspectBare TRL/PEFT/bitsandbytes (M5)Unsloth (M6)
Training speed1× (reference)~2× claimed — measure it
Peak VRAMreferenceup to ~70% less claimed — measure it
Dependency surfacethe standard stack+ a library that patches the stack
Low-level controltotal, line by lineabstracted behind fused kernels
Code portabilityplain TRL, reads anywherea bit tied to Unsloth
Output artifact4-bit base + LoRA adapterthe same

You gain speed and VRAM headroom; you give up a little vendor-neutrality and a little low-level control. That balance is precisely why Unsloth is optional in this course rather than baked into the happy path.

Config-as-code: the same fine-tune as one Axolotl YAML

Unsloth changes how the Python runs. Axolotl changes whether you write Python at all. It’s config-as-code: instead of authoring the pipeline in code, you describe the entire run in a single declarative YAML — base model, LoRA/QLoRA, dataset and format, SFT hyperparameters, and the training type. Axolotl spans LoRA/QLoRA, DPO/ORPO/KTO, GRPO, and FSDP, so one file can drive a wide range of runs. It sits around 12k GitHub stars (directional, June 2026). Think of it as the “config-as-code / multi-GPU” counterpart to the course’s TRL code — a thing to read and run, not a competitor we teach in depth.

Here’s Anvil’s QLoRA expressed as configs/anvil-qlora.yml — the same run as train_sft(tier="qlora", use_lora=True), every choice declared:

# configs/anvil-qlora.yml — Anvil's QLoRA SFT as config-as-code (Module 6)
base_model: Qwen/Qwen3-4B-Base          # same base as the TRL/PEFT path

load_in_4bit: true                       # QLoRA: 4-bit NF4 base + a LoRA adapter
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]

chat_template: qwen3                      # tools formatted via the chat template, never by hand
datasets:
  - path: ../../data/anvil_messages_train.jsonl  # messages-shaped export (build_axolotl_messages.py)
    type: chat_template
    field_messages: messages
    field_tools: tools
train_on_inputs: false                    # completion-only loss: learn the assistant turn

sequence_len: 1024
micro_batch_size: 8
gradient_accumulation_steps: 2
num_epochs: 1
learning_rate: 0.0002
lr_scheduler: cosine
output_dir: ./outputs/anvil-axolotl-qlora
seed: 0

Read it next to anvil/config.py and anvil/data.py and the mapping is one-to-one: base_model is MODEL_ID, lora_r: 16 is lora_config().r, train_on_inputs: false is completion_only_loss=True, and field_tools: tools is the same tools column the chat template renders. One difference from the Python path: the TRL run builds the {messages, tools} rows in memory with anvil.data.row_to_messages, but Axolotl reads the conversation straight from disk — so it points at a messages-shaped export, which you regenerate from the same transform with PYTHONPATH=module-06 python data/build_axolotl_messages.py. Same conversations, not a second drifting copy. You launch it with one CLI command — as of axolotl 0.17.0 (June 2026):

axolotl train configs/anvil-qlora.yml   # needs a CUDA GPU + `pip install axolotl==0.17.0`

⚠️ Verify the exact YAML keys and the CLI command against the Axolotl 0.17.0 docs at run time. The schema evolves between releases — same freshness posture as the FastLanguageModel signature. Don’t copy a key name from a year-old blog post.

The pedagogical point: same QLoRA, same Anvil, three front-ends. Plain TRL/PEFT Python, the same Python accelerated by Unsloth, or a declarative Axolotl YAML. You’re reading one recipe in three shapes. Axolotl shines when you want to version and share a config as a reviewable file, or scale multi-GPU with FSDP without rewriting code; Python shines when you want fine-grained control and step-by-step readability. (Axolotl isn’t alone in this niche — Llama-Factory is another config-driven framework in the same register. We mention it; we don’t teach it.)

flowchart TB
    a["TRL / PEFT / bitsandbytes<br/>(Python, M5)"] --> run
    b["Unsloth<br/>(Python, accelerated)"] --> run
    c["Axolotl<br/>(anvil-qlora.yml, config-as-code)"] --> run
    run["Anvil QLoRA training run"] --> art["4-bit base + LoRA adapter"]

Pick the front-end that fits your workflow — the fine-tune is the same, and none of them is required by this course.

TRL/PEFT (raw)UnslothAxolotl
InterfacePython codePython code + acceleratorYAML config
Speedreference~2× claimed (measured)depends on backend
VRAMreferenceup to ~70% less claimeddepends on backend
Lock-in / neutralitynone — standard stacklibrary patches the stackdedicated config framework
Low-level controltotalabstracteddeclarative
Multi-GPU / scalingmanual via acceleratevia Unsloth + accelerateFSDP built into the YAML
When to reach for itfull control, debuggable line by linea run slow enough to cost real wall-clocka config you can review in a PR and scale

That table is the decision the rest of this module rests on.

Vendor-neutrality: take it or leave it

This course is vendor-neutral by design. The canonical path stays TRL + PEFT + bitsandbytes (Modules 3–5) — the stack every fine-tuning engineer can read, with zero lock-in. Unsloth and Axolotl are optional layers on top: you reach for them when the gain (speed, VRAM, a shareable config) outweighs the cost (an extra dependency, a little less control, the risk of library churn).

What you gain. Unsloth buys you speed and VRAM headroom on the same free GPU — which makes bigger models or longer runs feasible on a T4, and is the lever behind the “80% less VRAM for GRPO” claim you’ll lean on in Module 10. Axolotl buys you a versionable, shareable, reproducible config (one YAML plus one commit equals one run) and multi-GPU scaling via FSDP without rewriting the pipeline.

What you give up. A little neutrality — your code becomes tied to a third-party library that patches the stack — and a little low-level control: Unsloth’s fused kernels abstract away what the bare TRL code exposes line by line, and an Axolotl YAML hides the training mechanics behind keys. Plus a real freshness cost: Unsloth ships often, its FastLanguageModel signature moves, and — the trap to internalize — its PyPI version is date-style (2026.6.7) while its GitHub tags look like v0.1.x-beta. Those are two different numbering schemes. Pin the PyPI version (pip install 'unsloth==2026.6.7'), and never pin a GitHub tag string to PyPI — pip will simply fail.

Here’s the opinion, stated plainly: I keep tier="qlora" as the canonical, copy-pasteable path I can debug line by line — and I switch to tier="unsloth" when a run is slow enough to cost me real wall-clock, which on a free T4 is often. I reach for an Axolotl YAML when I want a fine-tune I can hand a teammate as one reviewable file. None of those three is the “right” answer; the right answer is whichever cost you’re trying to pay down.

Build it: replay Anvil’s QLoRA through Unsloth, and measure it

Goal: add tier="unsloth" to config.py, replay the Module-5 QLoRA on the same free T4, measure the speedup and VRAM drop, and provide an equivalent Axolotl YAML.

What you’ll see: the loss goes down as usual, and at the end the notebook prints a comparison table — wall-clock and peak VRAM (torch.cuda.max_memory_allocated()) for tier="qlora" (M5) versus tier="unsloth" (M6) — showing a measured speedup. The number is yours; don’t trust an invented constant.

The full, tested code lives in finetune-prod-labs/module-06. The offline smoke test runs with no GPU and no network, so you can verify the plumbing before you spend a single GPU-minute.

Step 1 — Open a T4 runtime and install. Open the module notebook on Colab or Kaggle with a T4 GPU. The repo uses one shared root pyproject.toml + uv.lock for the core training stack; GPU tooling is installed per-runtime. Install Unsloth on top, and (optionally) Axolotl:

uv pip install "unsloth==2026.6.7"     # CUDA only; brings its own compatible trl
uv pip install "axolotl==0.17.0"       # optional — only for the config-as-code path

Confirm the GPU with torch.cuda.get_device_name() (you want a T4), and make sure HF_TOKEN is set (.env or a Colab secret). Re-check both PyPI versions on the day you run this — Unsloth especially ships fast.

Step 2 — Implement tier="unsloth" in config.py (this is the frozen increment). Add the if tier == "unsloth": return _load_unsloth() route at the top of get_model_and_tokenizer, plus the _load_unsloth() loader shown above (and the optional unsloth_lora() helper — train_sft doesn’t call it; it applies LoRA through SFTTrainer’s peft_config=lora_config() like every other tier). Copy the FastLanguageModel.from_pretrained signature verbatim from a current official Unsloth notebook for Qwen3-4B. All model/PEFT choices stay in config.py — don’t duplicate them in any script.

Step 3 — Replay the training. Run the unchanged train_sft.py with the new tier:

from anvil.train_sft import train_sft
trainer = train_sft(tier="unsloth", use_lora=True)

It calls get_model_and_tokenizer("unsloth"), builds the same SFTConfig, and feeds the Unsloth model to the standard SFTTrainer. TRL v1 idioms only — do not regress to tokenizer= or max_seq_length even if a notebook you copied shows them.

Step 4 — Measure the speedup. Wrap each run in torch.cuda.reset_peak_memory_stats() and a wall-clock timer, for tier="qlora" (the M5 replay) and tier="unsloth", then print the comparison. You’ll see something like this (your numbers will differ):

tier      wall-clock    peak VRAM
qlora     31m 40s       14.8 GB     (Module 5 replay)
unsloth   15m 10s        9.6 GB     (~2.1x faster, ~35% less peak VRAM — on my T4)

Report it as “on my run,” dated. A vendor’s 2× is their setup; this is yours.

Step 5 — The equivalent Axolotl recipe (optional path). The repo ships configs/anvil-qlora.yml (shown earlier). Launch it with axolotl train configs/anvil-qlora.yml. The expected result: the same kind of LoRA adapter, produced from one declarative file. This path is optional — it’s a side road, not the default.

Step 6 — The offline smoke test. The smoke test never touches a GPU. It verifies the vendor-neutral plumbing: asking for the unsloth tier without Unsloth installed raises a clear, actionable error, and the Axolotl recipe is valid config-as-code that matches the plain-TRL run.

def test_unsloth_tier_gives_actionable_error_when_absent():
    try:
        import unsloth  # noqa: F401
        pytest.skip("Unsloth installed — accelerated path covered by the gpu test")
    except ImportError:
        pass
    with pytest.raises(RuntimeError, match=r"unsloth"):
        config.get_model_and_tokenizer("unsloth")


def test_axolotl_recipe_matches_the_trl_run():
    recipe = yaml.safe_load((MODULE_DIR / "configs" / "anvil-qlora.yml").read_text())
    assert recipe["base_model"] == config.MODEL_ID          # same base as the TRL path
    assert recipe["adapter"] == "qlora" and recipe["load_in_4bit"] is True
    assert recipe["lora_r"] == config.lora_config().r       # same LoRA rank
    assert recipe["train_on_inputs"] is False               # completion-only loss

Run it from the repo root:

uv run pytest module-06/tests/

The accelerated run itself is a gpu-marked test, skipped cleanly when no CUDA device is present — so CI stays green without a GPU, and Axolotl is never required to run the suite.

What this lab does NOT do, on purpose. No mandatory switch — tier="qlora" stays the default; Unsloth and Axolotl are options. No GRPO (Module 10) — we state the “80% less VRAM for GRPO” claim and forward-reference it, nothing more. No formal evaluation (Module 7) — we watch speed, VRAM, and the loss going down, not base-vs-fine-tuned quality. No merge_and_unload / GGUF / AWQ / GPTQ / serving (Modules 12–13) — we only note that the Unsloth artifact is the same 4-bit base + adapter, so the tail of the pipeline is unchanged. And no FastLanguageModel signature recited from memory — copied verbatim from a current notebook.

Try it yourself:

  1. Actually launch the Axolotl run (axolotl train configs/anvil-qlora.yml) and compare its adapter and wall-clock to the Unsloth run and the bare-TRL run. Confirm all three produce the same kind of artifact.
  2. Measure Unsloth’s speedup on a longer sequence or a one-notch-bigger model — the gain depends on the workload. Quantify where Unsloth helps most.

In production

Out of the lab, choosing an accelerator is a deliberate cost/time trade — and you own it. Unsloth can make a long run (a bigger model, GRPO, many epochs) feasible on a free GPU, or cut the bill on a rented one; the “80% less VRAM for GRPO” claim exists for exactly that, and you measure it the day you run it (Module 10). Axolotl earns its place the moment a team wants a config that’s reviewable in a PR, reproducible (the YAML plus a commit equals a run), and scalable multi-GPU via FSDP without rewriting the pipeline.

The flip side you manage in production: pin the version (Unsloth ships often; the signature and format can move between releases — pin it, dated), validate that the output is identical to the bare-TRL path (same adapters, same artifact to serve in Module 13), and keep the bare TRL/PEFT path as your debugging reference for when an accelerator behaves strangely. You don’t want to depend on a third-party library just to exist. Concretely: I benchmark unsloth against qlora once per stack bump — if the speedup on my T4 drops below ~1.5×, I read the release notes before I trust it again.

And the forward look: in Module 10, this same tier="unsloth" is what makes Anvil’s GRPO affordable on the free path — to be measured, not assumed.

Concept check

Why this matters. This module gives you three reusable judgments: Unsloth is a drop-in, optional accelerator (same QLoRA, same artifact, just faster); Axolotl is config-as-code (one YAML drives the whole pipeline); and you can reason about the vendor-neutrality trade (what each layer gains and costs). Concretely, tier="unsloth" is one option plugged into config.py — the default stays tier="qlora" — and it’s reused to accelerate Anvil’s GRPO in Module 10. The questions below stay inside what this module actually taught (no formal evaluation — that’s Module 7).

  1. A teammate is nervous about switching Anvil from tier="qlora" to tier="unsloth": “won’t that change my QLoRA, and break the adapter I’ll later merge and serve?” What’s the correct answer?

    • A. Yes — Unsloth uses a different loss, so the adapter isn’t compatible.
    • B. No — it’s the same QLoRA (4-bit NF4 base + LoRA adapters, same SFT loss), executed with optimized kernels; the output artifact is the same.
    • C. Yes — Unsloth produces a fully-quantized deployment model, not an adapter.
    • D. No — but only because Unsloth secretly disables LoRA and trains the full model.
  2. A reader concludes: “I can’t fine-tune on a free T4 without Unsloth.” Is that right?

    • A. Right — bitsandbytes QLoRA needs Unsloth to fit 16 GB.
    • B. Right — only Unsloth’s fused kernels make 4-bit training possible.
    • C. Wrong — the bare TRL/PEFT/bitsandbytes QLoRA from Module 5 already fits a free T4; Unsloth just makes it faster.
    • D. Wrong — but only if you rent an A100.
  3. A team wants a fine-tune recipe they can review in a pull request, reproduce from a commit, and scale across multiple GPUs without rewriting code. Which front-end fits best?

    • A. Bare TRL/PEFT Python — most control.
    • B. Unsloth — fastest single-GPU run.
    • C. An Axolotl YAML — config-as-code, one reviewable file, FSDP built in.
    • D. None — you must hand-write an accelerate config.
  4. You copy a FastLanguageModel.from_pretrained(...) call from a 2025 blog post and it raises a TypeError about unexpected arguments. What’s the most likely cause and fix?

    • A. Your GPU is too small — switch to an A100.
    • B. The signature moved between Unsloth releases — copy it from a current official notebook and pin the version.
    • C. You must downgrade transformers to 4.x.
    • D. You forgot to pass tokenizer= to the trainer.
  5. A developer runs pip install unsloth==v0.1.3-beta (a GitHub tag they saw) and pip can’t find it. What’s going on?

    • A. Unsloth is unpublished on PyPI — install from source only.
    • B. The PyPI version is date-style (2026.6.7) and diverges from the GitHub tags (v0.1.x-beta); pin the PyPI version.
    • C. They need --pre for beta releases.
    • D. The package was renamed; there is no unsloth on PyPI.

Answers.

  1. B. Unsloth changes execution, not method. It’s the same QLoRA (4-bit NF4 base + LoRA, same next-token SFT loss) run with fused kernels, and it emits the same artifact — a 4-bit base plus a LoRA adapter — so the merge/quantize/serve tail (Modules 12–13) is unchanged. A is wrong (same loss), C confuses training-time QLoRA with a deployment format, D is nonsense.
  2. C. The Module-5 QLoRA, built from plain TRL/PEFT/bitsandbytes, already fits a free 16 GB T4 — that was the whole point of Module 5. Unsloth is an optional accelerator that makes the run faster (claimed ~2×, up to ~70% less VRAM, as of June 2026 — measure it). It is not a precondition for the free path.
  3. C. That’s Axolotl’s sweet spot: config-as-code in one declarative YAML, versionable and reviewable, with FSDP multi-GPU built into the file. Bare Python (A) gives control but no built-in scaling; Unsloth (B) accelerates a single-GPU run but isn’t a config front-end.
  4. B. Unsloth’s FastLanguageModel signature moves between releases, and old tutorials drift out of date. The fix is to copy the exact call from a current official notebook and pin the version (unsloth==2026.6.7 here) — and, while you’re at it, don’t let an old notebook tempt you back into tokenizer=/max_seq_length. The error isn’t about GPU size (A) or transformers 4.x (C); D is the unrelated banned idiom.
  5. B. Unsloth’s PyPI releases are date-style (2026.6.7) while its GitHub tags look like v0.1.x-beta — two different numbering schemes. You pin the PyPI version; a GitHub tag string is not a PyPI version, so pip fails. The package is on PyPI (A, D wrong), and --pre doesn’t conjure a nonexistent version (C).

Common pitfalls.

  • Believing you “need” Unsloth for the free path. You don’t — the bare QLoRA from Module 5 already fits a T4. Unsloth is an optional accelerator (as of unsloth 2026.6.7, June 2026). Treating it as mandatory is the headline misconception of this module.
  • Reciting a FastLanguageModel signature from memory or an old tutorial. Its arguments change between releases — copy it verbatim from a current official notebook, and don’t regress processing_class/max_length while you’re in there.
  • Confusing Unsloth’s PyPI version with its GitHub tags. PyPI is date-style (2026.6.7); GitHub tags are v0.1.x-beta. Pin the PyPI version and note the divergence — re-check it on the day you install.

Key takeaways

  • Unsloth is an optional, drop-in accelerator, not a new method and not a requirement: the same QLoRA (4-bit NF4 base + LoRA, same SFT loss), the same output artifact — just faster, fused-kernel execution.
  • The “2× faster, up to 70% less VRAM” claim (and “80% less VRAM for GRPO”) is a claim — measure it on your hardware, report it as “on my run,” and date it (as of unsloth 2026.6.7, June 2026).
  • The FastLanguageModel signature moves between releases — copy it from a current official notebook, never from memory, and don’t regress to TRL’s old tokenizer=/max_seq_length idioms.
  • Unsloth lives behind tier="unsloth" in config.py and returns the same (model, tokenizer) as every other tier, so train_sft.py and the SFTTrainer/SFTConfig run unchanged.
  • Axolotl is config-as-code: one declarative YAML drives the whole pipeline (LoRA/QLoRA, DPO/ORPO/KTO, GRPO) and scales multi-GPU with FSDP — the same Anvil fine-tune as a reviewable file.
  • The canonical path stays bare TRL/PEFT/bitsandbytes with tier="qlora" as the default; Unsloth and Axolotl are accelerators you take or leave — vendor-neutrality by design.
  • Unsloth’s PyPI version is date-style (2026.6.7) and diverges from its GitHub tags (v0.1.x-beta) — pin the PyPI version.

What’s next

Anvil trains fast now — but is it actually any good? Module 7 builds the task-grounded evaluation that proves your fine-tune beats the base model: JSON validity, function-name accuracy, argument match, and anti-forgetting checks. Speed is worthless if the model got worse, so we measure quality before we trust the run.

Want the full fine-tuning concept-check bank and the next module in your inbox? Subscribe here — it’s free, like everything in this series.

Links: Course index · ← Module 5: Make It Fit a Free GPU: QLoRA from Scratch · Module 7: Is It Any Better? Task-Grounded Evaluation → · Lab code for this module

References