Unsloth is a Python stack for LoRA and QLoRA fine-tuning. This guide launches a Massed Compute GPU VM, installs Unsloth in a venv, trains an ungated instruct model on a public Hugging Face dataset, and writes a LoRA adapter you can reload later.
The walkthrough uses 4-bit QLoRA (load_in_4bit=True) on Qwen/Qwen2.5-0.5B-Instruct and the first 1,024 rows of yahma/alpaca-cleaned. That is a smoke you can finish in minutes, not a production 8B run. Swap the model name once the adapter path is green. This was validated on an NVIDIA L40 (48 GB) on August 25, 2026.
If you already follow Fine-Tune LLMs with QLoRA on a Cloud GPU, that post is PEFT + bitsandbytes. This post is Unsloth’s FastLanguageModel path. Do not copy that recipe’s torch and TRL pins. Axolotl and LLaMA-Factory are separate later guides — not this workflow.
| Component | Version | Purpose |
|---|---|---|
| Ubuntu Server | 24.04 LTS | Image 184: NVIDIA driver 580.126.16 |
| Unsloth | 2026.8.20 | FastLanguageModel + Unsloth gradient checkpointing |
| PyTorch | 2.11.0+cu130 | CUDA 13.0 wheels pulled by pip install unsloth |
| TRL | 0.24.0 | SFTTrainer / SFTConfig (processing_class=tokenizer) |
| PEFT | 0.20.0 | LoRA adapter (Unsloth wraps get_peft_model) |
| Transformers | 5.5.0 | Qwen2 tokenizer + generate |
| bitsandbytes | 0.50.1 | 4-bit NF4 base weights |
| Model (validated) | Qwen/Qwen2.5-0.5B-Instruct | Ungated instruct model, load_in_4bit=True |
| Dataset | yahma/alpaca-cleaned | 51,760 rows; this run used 1,024 |
| Resource | This walkthrough | Notes |
|---|---|---|
| GPU | L40 48 GB (gpu_1x_l40) |
Validated. 0.5B QLoRA used ~1.2 GB; 7B–8B QLoRA is why you want 48 GB |
| System RAM | 72 GB | SKU ships 72 GiB |
| vCPU | 14 | SKU ships 14 vCPU |
| Storage | 625 GB | Hugging Face cache plus adapter |
| Network | 1 Gbps | First pip install unsloth pulls torch (~0.5 GB) plus CUDA wheels |
Massed Compute VM Pricing
Lead with the L40 this guide was tested on. 80 GB cards are listed for 70B QLoRA we did not run here.
Pricing fetched from the Massed Compute inventory API on August 25, 2026.
| SKU | Description | vCPU | RAM | Storage | Price | Capacity |
|---|---|---|---|---|---|---|
gpu_1x_l40_spot |
1x L40 (48GB) [Spot] | 14 | 72 GiB | 625 GB | $0.78/hr | 22 |
gpu_1x_6000_ada |
1x RTX 6000 ADA (48GB) | 12 | 72 GiB | 350 GB | $0.79/hr | 4 |
gpu_1x_l40 |
1x L40 (48GB) | 14 | 72 GiB | 625 GB | $0.86/hr | 22 |
gpu_1x_A100_SXM4 |
1x A100 SXM4 (80GB) | 14 | 100 GiB | 625 GB | $1.38/hr | 12 |
gpu_1x_DGX_A100 |
1x DGX A100 (80GB) | 16 | 120 GiB | 1000 GB | $1.38/hr | 2 |
gpu_1x_l40. Use on-demand if the job cannot restart. The L40 is the card this run used — see NVIDIA L40 GPU Best Use Cases. For 80 GB QLoRA (70B-class) see NVIDIA A100 GPU Best Use Cases. Training vs inference card sizing: The Best GPU for LLM Inference Without Overpaying. After you have an adapter, serve it with SGLang or vLLM — that is a different post.Step-by-Step Deployment
Image 184 logs in as Ubuntu (capital U). If SSH offers extra keys and then fails, add -o IdentitiesOnly=yes. Image 184 ships NVIDIA drivers, not a ready Python venv. Install python3.12-venv (or python3-venv) before python3 -m venv.
Launch GPU VM
# Launch via Massed Compute dashboard or API
# Product: gpu_1x_l40
# Image: 184 (Ubuntu Server 24.04 w/ Drivers)
# SSH Key: attach your public key
# Instance name: unsloth-finetune
Wait until the VM is running and copy the SSH details. This run landed in us-central-3 (Des Moines).
Verify GPU Access
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader'
This run printed NVIDIA L40, 46068 MiB, 580.126.16.
Install Unsloth
Pin whatever pip actually installs. This VM printed Unsloth 2026.8.20, torch 2.11.0+cu130, TRL 0.24.0. Import unsloth before trl / transformers / peft or Unsloth warns that patches may be skipped.
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
set -euxo pipefail
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3.12-venv
python3 -m venv ~/unsloth_env
~/unsloth_env/bin/pip install -U pip
~/unsloth_env/bin/pip install unsloth
~/unsloth_env/bin/python - <<'PY'
import unsloth, torch, trl, peft, transformers, bitsandbytes
print("unsloth", unsloth.__version__)
print("torch", torch.__version__, "cuda", torch.version.cuda)
print("trl", trl.__version__)
print("peft", peft.__version__)
print("transformers", transformers.__version__)
print("bnb", bitsandbytes.__version__)
print("gpu", torch.cuda.get_device_name(0))
PY
EOF
Unsloth’s current install page is Install Unsloth via pip. The older docs.unsloth.ai/get-started/installing-+-updating URL 404s.
Dataset format
Alpaca-style rows are instruction, input, output. Unsloth’s notebooks format them into a single text field and append the tokenizer EOS. Qwen2.5 Instruct’s EOS is <|im_end|> (id 151645 on this run). TRL 0.24 rejects a fake <EOS_TOKEN> that is not in the vocab.
This run loaded yahma/alpaca-cleaned (51,760 train rows) and kept the first 1,024. Full-epoch training is num_train_epochs=1 with max_steps unset — not this smoke.
LoRA / QLoRA config
load_in_4bit=True is QLoRA (4-bit base + LoRA adapters). r=16, lora_alpha=16, lora_dropout=0, bias="none", use_gradient_checkpointing="unsloth". Target modules: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj.
Unsloth printed 8,798,208 trainable parameters of 502,830,976 (1.75%). max_seq_length=2048. Padding-free training auto-enabled.
Train
TRL 0.24’s SFTTrainer takes processing_class=tokenizer, not tokenizer=. Batch 2 × grad accum 4 = effective 8. max_steps=60, optim="adamw_8bit", learning_rate=2e-4, report_to="none".
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'~/unsloth_env/bin/python ~/unsloth_train.py'
Put unsloth_train.py on the VM (same logic as below). First step was slower (~6.5 s); later steps ~1.05 s.
import unsloth # before trl / transformers / peft
from datasets import load_dataset
from trl import SFTConfig, SFTTrainer
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen2.5-0.5B-Instruct",
max_seq_length=2048,
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
)
alpaca = (
"Below is an instruction that describes a task, paired with an input that "
"provides further context. Write a response that appropriately completes "
"the request.\n\n### Instruction:\n{}\n\n### Input:\n{}\n\n### Response:\n{}"
)
eos = tokenizer.eos_token # <|im_end|> on Qwen2.5 Instruct
def fmt(batch):
texts = [
alpaca.format(i, x, o) + eos
for i, x, o in zip(batch["instruction"], batch["input"], batch["output"])
]
return {"text": texts}
ds = load_dataset("yahma/alpaca-cleaned", split="train").select(range(1024))
ds = ds.map(fmt, batched=True)
trainer = SFTTrainer(
model=model,
processing_class=tokenizer,
train_dataset=ds,
args=SFTConfig(
dataset_text_field="text",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=60,
learning_rate=2e-4,
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir="outputs",
report_to="none",
eos_token=eos,
),
)
trainer.train()
model.save_pretrained("lora_model")
tokenizer.save_pretrained("lora_model")
Save the adapter
model.save_pretrained("lora_model") wrote:
| File | Size |
|---|---|
adapter_config.json |
1,313 B |
adapter_model.safetensors |
35,237,104 B (~33.6 MiB) |
tokenizer.json / tokenizer_config.json |
tokenizer copy |
That is the adapter, not a merged 16-bit model. Merge is optional (FAQ). Checkpoint-60 also landed under outputs/.
Generate from the adapter
FastLanguageModel.for_inference(model)
prompt = alpaca.format("Explain LoRA fine-tuning in one sentence.", "", "")
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
out = model.generate(**inputs, max_new_tokens=64, use_cache=True)
print(tokenizer.decode(out[0], skip_special_tokens=True))
This smoke returned a completion. Quality after 60 steps on 0.5B is not a product claim — it only proves generate runs on the adapter.
GPU util
nvidia-smi memory.used:
| When | Used (MiB) / total |
|---|---|
| Idle | 541 / 46068 |
| After 4-bit load | 1105 / 46068 |
| After tokenize, before train | 1139 / 46068 |
| After 60 steps + generate | 1213 / 46068 |
Used rose during the job. 0.5B QLoRA barely taxes a 48 GB L40. Size the SKU for the 7B–8B job you actually want, not this smoke.
Unsloth vs PEFT QLoRA vs Axolotl / LLaMA-Factory
| Path | What it is | Use this post? |
|---|---|---|
| Unsloth | FastLanguageModel, Unsloth checkpointing, TRL SFT |
Yes — this guide |
| PEFT + bitsandbytes | Generic Hugging Face QLoRA | Existing QLoRA guide |
| Axolotl | YAML training launcher | Later cluster post (MAR-45) — do not copy that workflow here |
| LLaMA-Factory | Web UI / config training | Later cluster post (MAR-47) |
Unsloth’s own docs claim faster kernels and lower VRAM than stock PEFT. This VM did not run the PEFT recipe side-by-side. Do not quote a 2× number from this article.
VRAM / GPU tier
| Model (QLoRA 4-bit) | This article | SKU |
|---|---|---|
| 0.5B Instruct | Measured: ~1.2 GB used on L40 | 24 GB is enough; we used 48 GB |
| 3B-class | Not run. Typical QLoRA fits 24 GB | A30 / A5000 when in stock |
| 7B–8B Instruct | Not run. Unsloth lists ~16 GB class as a floor | L40 48 GB is the comfortable card |
| 70B QLoRA | Not run | 80 GB A100 / H100 — see the A100 product post |
CTA matches the demonstrated 0.5B-on-L40 plus the 8B recommendation: launch gpu_1x_l40.
Measured result
Same L40, Unsloth 2026.8.20, Qwen2.5-0.5B-Instruct 4-bit, yahma/alpaca-cleaned (1,024 rows), 60 steps, August 25, 2026.
| Metric | Value |
|---|---|
| Trainer wall clock | 71.24 s (train_runtime 69.83 s) |
| Steps | 60 (epoch 0.47 of the 1,024-row slice) |
| Train loss | 1.323 (step 1: 1.926) |
| Throughput | 6.874 samples/s · 0.859 steps/s |
Peak nvidia-smi used |
1213 MiB of 46068 MiB |
| Trainable params | 8,798,208 / 502,830,976 (1.75%) |
| Adapter | adapter_model.safetensors 35.2 MB |
| Generate | One completion from the adapter (smoke quality) |
This is a 0.5B smoke, not an 8B overnight job and not a vs-PEFT bake-off.
Troubleshooting
SSH fails or keeps asking for a password. Image 184’s user is Ubuntu, not ubuntu. Use -o IdentitiesOnly=yes. Refresh host keys with ssh-keygen -R YOUR_VM_IP if the instance was relaunched on a reused IP.
ensurepip is not available. Image 184 needs sudo apt-get install -y python3.12-venv (or python3-venv) before python3 -m venv.
Unsloth should be imported before [trl, transformers, peft]. Put import unsloth at the top of the train script. This run still trained after a first attempt that imported TRL first; import order is the supported path.
eos_token ('<EOS_TOKEN>') is not found in the vocabulary. TRL 0.24. Pass Qwen’s real EOS (<|im_end|>) in SFTConfig(eos_token=...) and append that string to each text example. Do not append a placeholder that is not in the tokenizer.
SFTTrainer() got an unexpected keyword argument 'tokenizer'. TRL 0.24 uses processing_class=tokenizer. Follow Unsloth’s current notebook, not an older PEFT gist.
Gated Llama 401. This guide uses ungated Qwen2.5 Instruct. Llama 3.x needs HF_TOKEN in the environment before from_pretrained.
Out of memory on 7B–8B. Lower per_device_train_batch_size, raise gradient_accumulation_steps, shorten max_seq_length, keep load_in_4bit=True, or move to 48 GB / 80 GB. This 0.5B run is not an OOM test.
Xformers = None in the Unsloth banner. This L40 trained without xformers. If a later Unsloth/torch pair demands it, install the xformers wheel Unsloth’s installer selects for that torch — do not mix random versions.
Loss goes to 0. Overfit on a tiny slice. This smoke’s mean loss was 1.32 after 60 steps. Use a larger dataset and 1–3 epochs for a real adapter.
Fine-Tune LLMs Faster with Unsloth
Launch NVIDIA L40 instances for Unsloth QLoRA fine-tuning. Get 48GB VRAM, NVMe storage, and per-second billing.
Think it. Build it. Scale it.
Quick Setup Reference
# 1. Launch gpu_1x_l40, image 184
# 2. Verify GPU
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'nvidia-smi'
# 3. venv + Unsloth
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
sudo apt-get update -qq && sudo apt-get install -y python3.12-venv
python3 -m venv ~/unsloth_env
~/unsloth_env/bin/pip install -U pip unsloth
EOF
# 4. Train + save adapter (script from step 6)
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'~/unsloth_env/bin/python ~/unsloth_train.py'
# 5. Confirm adapter files
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'ls -l ~/unsloth-run/lora_model/adapter_*.json ~/unsloth-run/lora_model/*.safetensors'
Frequently Asked Questions
01What did this guide actually run?
gpu_1x_l40, image 184, Unsloth 2026.8.20, Qwen/Qwen2.5-0.5B-Instruct 4-bit QLoRA, yahma/alpaca-cleaned (1,024 of 51,760 rows), 60 SFT steps, adapter on disk. Trainer wall 71.24 s. Peak used 1213 MiB.
02Is this faster than the PEFT QLoRA post?
Not measured here. Use Fine-Tune LLMs with QLoRA on a Cloud GPU for PEFT. This post is Unsloth.
03Should I merge the adapter into the base model?
Not for the smoke. Keep adapter_config.json + adapter_model.safetensors and load with Unsloth or PEFT. Merge when you need a single Hugging Face folder for a server that does not load adapters.
04What about 7B–8B?
Same install. Change model_name to an ungated instruct checkpoint (or an unsloth/*-bnb-4bit repo). Expect more VRAM and wall-clock. This VM did not train 8B.
05Can I QLoRA a 70B on this L40?
Not this walkthrough. Use 80 GB (A100) or multi-GPU. Full fine-tune of 7B+ is out of scope.
06Do I need a Hugging Face token?
Not for Qwen2.5-0.5B-Instruct or yahma/alpaca-cleaned. Gated Llama weights need HF_TOKEN.
07What’s the difference between spot and on-demand?
Spot is cheaper and can be interrupted. This run used on-demand gpu_1x_l40. Use on-demand if a long epoch cannot restart.
Recipe tested on August 25, 2026.











