Fine-tune LLMs with Axolotl on a Massed Compute 2× H100.

Fine-Tune LLMs with Axolotl on Multi-GPU VMs (2026 Guide)

Axolotl is a YAML-driven fine-tuning stack. This guide launches a Massed Compute multi-GPU VM, installs Axolotl in a venv, trains an ungated instruct model on a public Hugging Face dataset from one config file, and writes a LoRA adapter you can reload later.

GPUNVIDIAH100AxolotlLoRAQLoRAFine-TuningMulti-GPUUbuntu

The walkthrough uses 4-bit QLoRA (load_in_4bit: true, adapter: qlora) 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 or 70B run. Swap the model name once the adapter path is green. This was validated on two NVIDIA H100 PCIe (80 GB) on August 25, 2026 — first with CUDA_VISIBLE_DEVICES=0, then with Axolotl’s torchrun launcher on both cards. That is Axolotl multi-GPU training on a Massed Compute VM, not a single-GPU notebook.

If you already follow Fine-Tune LLMs Faster with Unsloth on GPU Cloud, that post is Unsloth’s FastLanguageModel path. The existing Fine-Tune LLMs with QLoRA on a Cloud GPU post is PEFT + bitsandbytes. This post is Axolotl’s axolotl train config.yml path. Do not copy those install commands. DeepSpeed ZeRO is a later cluster post — named here, not walked through.

Technology Stack
Component Version Purpose
Ubuntu Server 24.04 LTS Image 184: NVIDIA driver 580.126.16
Axolotl 0.18.0 YAML trainer (axolotl train)
PyTorch 2.12.1+cu130 CUDA 13.0 wheels (UV_TORCH_BACKEND=cu130)
PEFT 0.19.1 LoRA adapter (peft_type: LORA)
Transformers 5.14.1 Qwen2 tokenizer + causal LM
bitsandbytes 0.49.1 4-bit NF4 base weights
Accelerate / torchrun bundled with Axolotl 1-GPU --launcher python; 2-GPU --launcher torchrun -- --nproc_per_node=2
Model (validated) Qwen/Qwen2.5-0.5B-Instruct Ungated instruct model, QLoRA
Dataset yahma/alpaca-cleaned 51,760 rows; this run used 1,024 (split: train[:1024])
System Requirements
Resource This walkthrough Notes
GPU 2× H100 PCIe 80 GB (gpu_2x_h100) Validated. 0.5B QLoRA used ~6 GB per card; 70B-class QLoRA / FSDP is why you want 80 GB × 2
System RAM 256 GB SKU ships 256 GiB
vCPU 40 SKU ships 40 vCPU
Storage 2500 GB Hugging Face cache plus adapters
Network 1 Gbps First uv pip install torch pulls CUDA 13 wheels

Massed Compute VM Pricing

Lead with the 2× H100 this guide was tested on. Single H100 and 2× L40 are listed for jobs that do not need two 80 GB cards.

Pricing fetched from the Massed Compute inventory API on August 25, 2026.

SKU Description vCPU RAM Storage Price Capacity
gpu_1x_l40 1x L40 (48GB) 14 72 GiB 625 GB $0.86/hr 14
gpu_2x_l40 2x L40 (48GB) 26 144 GiB 1250 GB $1.72/hr 5
gpu_1x_h100_spot 1x H100 (80GB) [Spot] 20 128 GiB 1250 GB $2.45/hr 3
gpu_1x_h100 1x H100 (80GB) 20 128 GiB 1250 GB $2.73/hr 3
gpu_2x_h100_spot 2x H100 (80GB) [Spot] 40 256 GiB 2500 GB $4.90/hr 1
gpu_2x_h100 2x H100 (80GB) 40 256 GiB 2500 GB $5.46/hr 1
Spot pricing available: Spot instances can be interrupted. This walkthrough used on-demand gpu_2x_h100. Use on-demand if the job cannot restart. Hopper / H100 context: Llama 3.1 Benchmark by GPU Type and the H100 section in Why RAG Systems Rely on NVIDIA GPUs. Transformer Engine / FP8 background: Why NVIDIA Leads in AI. 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. This run used Axolotl’s documented uv path on top of that package.

1

Launch GPU VM

# Launch via Massed Compute dashboard or API
# Product: gpu_2x_h100
# Image: 184 (Ubuntu Server 24.04 w/ Drivers)
# SSH Key: attach your public key
# Instance name: axolotl-multigpu

Wait until the VM is running and copy the SSH details.

2

Verify GPU Access

ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
  'nvidia-smi -L; nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader'

This run printed two lines: NVIDIA H100 PCIe, 81559 MiB, 580.126.16. nvidia-smi -L must show 2 GPUs before you treat this as a multi-GPU host.

Lock the firewall to SSH only. No public training UI is required.

ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
echo y | sudo ufw enable
sudo ufw status
EOF
3

Install Axolotl

Pin whatever uv pip actually installs. This VM printed Axolotl 0.18.0, torch 2.12.1+cu130, PEFT 0.19.1, Transformers 5.14.1, bitsandbytes 0.49.1. Official docs: Axolotl installation. This run installed axolotl without the DeepSpeed extra — DeepSpeed configs are a later post.

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 curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
export UV_TORCH_BACKEND=cu130
uv venv --python 3.12 ~/axolotl_env
source ~/axolotl_env/bin/activate
uv pip install -U pip packaging setuptools wheel ninja
uv pip install torch==2.12.1 torchvision
uv pip install --no-build-isolation axolotl
python - <<'PY'
import axolotl, torch, peft, transformers, bitsandbytes as bnb
import importlib.metadata as md
print("axolotl", md.version("axolotl"))
print("torch", torch.__version__, "cuda", torch.version.cuda, "gpus", torch.cuda.device_count())
print("peft", peft.__version__)
print("transformers", transformers.__version__)
print("bnb", bnb.__version__)
PY
axolotl --version || true
EOF
4

Config-file structure

Axolotl is one YAML. Model, dataset, adapter, and hyperparameters live in that file. type: alpaca matches yahma/alpaca-cleaned rows (instruction / input / output). split: train[:1024] keeps the smoke short. Full-epoch training is num_epochs: 1 with max_steps unset — not this smoke.

base_model: Qwen/Qwen2.5-0.5B-Instruct
load_in_4bit: true
adapter: qlora
datasets:
  - path: yahma/alpaca-cleaned
    type: alpaca
    split: train[:1024]
val_set_size: 0.0
output_dir: ./outputs/qlora-1gpu
sequence_len: 512
sample_packing: false
lora_r: 16
lora_alpha: 16
lora_dropout: 0.05
lora_target_linear: true
lora_mlp_kernel: false
lora_qkv_kernel: false
lora_o_kernel: false
lora_embedding_kernel: false
micro_batch_size: 2
gradient_accumulation_steps: 4
max_steps: 60
optimizer: adamw_torch
lr_scheduler: cosine
learning_rate: 0.0002
bf16: true
gradient_checkpointing: false
logging_steps: 1
warmup_steps: 5
use_wandb: false
attn_implementation: eager

load_in_4bit: true + adapter: qlora is QLoRA. r=16, lora_alpha=16. Target modules on this run: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj. Trainable 8,798,208 of 502,830,976 (1.75%). sequence_len: 512 for the smoke (11 of 1,024 rows dropped as longer than 512).

Default attention (SDPA) plus Axolotl’s auto LoRA kernels produced NaN grad_norm and loss 0 after step 1 on this torch 2.12.1 / bitsandbytes 0.49.1 stack. The YAML above sets attn_implementation: eager and turns the LoRA kernels off. That is the config that trained.

5

Single-GPU baseline

Same YAML. Pin one card. --launcher python skips Accelerate so you are not accidentally DDP on a hidden second GPU.

ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
set -euxo pipefail
export PATH="$HOME/.local/bin:$PATH"
source ~/axolotl_env/bin/activate
mkdir -p ~/axolotl-run
# put qlora.yml from step 4 on the VM
cd ~/axolotl-run
CUDA_VISIBLE_DEVICES=0 AXOLOTL_DO_NOT_TRACK=1 WANDB_DISABLED=true \
  axolotl train qlora.yml --launcher python --no-use-wandb --max-steps 60
EOF

This run: wall 75.75 s (train_runtime 59.69 s after dataset already cached). Loss 2.287 → 0.886 (mean 1.419). nvidia-smi peak 6335 MiB on GPU 0, 0 MiB on GPU 1.

6

Multi-GPU launch

Same YAML. Change output_dir so you do not overwrite the 1-GPU adapter. Axolotl’s documented launcher:

axolotl train qlora-2gpu.yml --launcher torchrun -- --nproc_per_node=2

Arguments after -- go to torchrun. This host has no GPU P2P; Axolotl logged P2P support not detected, setting NCCL_P2P_DISABLE=1 and still DDP’d (world_size: 2, ddp: true).

ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
set -euxo pipefail
export PATH="$HOME/.local/bin:$PATH"
source ~/axolotl_env/bin/activate
cd ~/axolotl-run
sed 's|output_dir: ./outputs/qlora-1gpu|output_dir: ./outputs/qlora-2gpu|' qlora.yml > qlora-2gpu.yml
AXOLOTL_DO_NOT_TRACK=1 WANDB_DISABLED=true \
  axolotl train qlora-2gpu.yml --launcher torchrun -- --nproc_per_node=2
nvidia-smi --query-gpu=index,memory.used,utilization.gpu --format=csv
EOF

This run: wall 81.71 s (train_runtime 60.32 s). Loss 2.206 → 1.119 (mean 1.398). Peak 6707 MiB and 37% / 30% util on both GPUs. Throughput 15.92 samples/s vs 8.04 on one GPU — about 2× samples per wall-clock step, not a 2× shorter 60-step job. A 0.5B smoke does not need two H100s; the dual-card launch is the pattern you keep for 7B–70B.

FSDP and DeepSpeed ZeRO are Axolotl options for models that do not fit DDP. They are not this YAML. DeepSpeed is a later cluster post.

7

Checkpoints / adapter on disk

axolotl train wrote:

File Size
adapter_config.json 1,102 B
adapter_model.safetensors 35,237,104 B (~33.6 MiB)
checkpoint-60/ same adapter plus optimizer / trainer state

That is the adapter, not a merged 16-bit model. Merge is optional (axolotl merge-lora, FAQ). Paths: ~/axolotl-run/outputs/qlora-1gpu/ and ~/axolotl-run/outputs/qlora-2gpu/.

8

GPU util

nvidia-smi memory.used during the 60-step jobs:

When GPU 0 GPU 1
Idle 0 / 81559 MiB 0 / 81559 MiB
1-GPU QLoRA peak 6335 MiB, 27% util 0 MiB
2-GPU QLoRA peak 6707 MiB, 37% util 6707 MiB, 30% util

Used rose on the cards that were supposed to work. 0.5B QLoRA barely taxes 80 GB. Size the SKU for the 7B–70B job you actually want, not this smoke.

Unsloth vs PEFT QLoRA vs Axolotl

Path What it is Use this post?
Axolotl One YAML, axolotl train, DDP via torchrun Yes — this guide
Unsloth FastLanguageModel, Unsloth checkpointing, TRL SFT Unsloth GPU cloud
PEFT + bitsandbytes Generic Hugging Face QLoRA Existing QLoRA guide
DeepSpeed ZeRO stages inside Axolotl or standalone Later cluster post — do not copy that workflow here
LLaMA-Factory Web UI / config training Later cluster post

This VM did not run Unsloth or the PEFT recipe side-by-side. Do not quote a speedup from this article.

VRAM / GPU tier

Model (QLoRA 4-bit) This article SKU
0.5B Instruct Measured: ~6.3 GB nvidia-smi on one H100 24 GB is enough; we used 2× 80 GB to show DDP
7B–8B Instruct Not run. Typical QLoRA fits 48 GB Start on 1× L40; move to 2× L40 or 1× H100 if you OOM
70B QLoRA / long context Not run 2× H100 80 GB (this SKU) or FSDP — not DeepSpeed in this post
Full fine-tune 7B+ Not run Multi-GPU; DeepSpeed / FSDP later

CTA: launch gpu_1x_l40 for 7B–8B QLoRA, gpu_2x_h100 when you need two 80 GB cards and a DDP launcher. This smoke used gpu_2x_h100 so the 2-GPU command is real.

Measured result

Same 2× H100, Axolotl 0.18.0, Qwen2.5-0.5B-Instruct QLoRA, yahma/alpaca-cleaned (1,024 rows), 60 steps, August 25, 2026.

Metric 1 GPU (CUDA_VISIBLE_DEVICES=0) 2 GPU (torchrun --nproc_per_node=2)
Process wall 75.75 s 81.71 s
train_runtime 59.69 s 60.32 s
Train loss (mean) 1.419 (2.287 → 0.886) 1.398 (2.206 → 1.119)
Throughput 8.041 samples/s 15.92 samples/s
Peak nvidia-smi 6335 / 81559 MiB on GPU 0 only 6707 / 81559 MiB on both GPUs
Trainable params 8,798,208 / 502,830,976 (1.75%) same
Adapter adapter_model.safetensors 35.2 MB same size

This is a 0.5B smoke, not an 8B overnight job and not a vs-Unsloth bake-off. Two GPUs processed about twice as many samples in the same 60 optimizer steps (epoch 0.47 vs 0.94).

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. This run also used uv venv --python 3.12.

loss: 0 and grad_norm: nan from step 2. Seen on this stack with default SDPA attention and Axolotl’s auto LoRA kernels (lora_*_kernel). Set attn_implementation: eager and lora_mlp_kernel / lora_qkv_kernel / lora_o_kernel / lora_embedding_kernel: false. Confirm grad_norm is finite on step 1 before you trust a long run.

QLoRA + 2 GPU fights bitsandbytes. This 2× H100 DDP QLoRA run completed. If a later bitsandbytes/torch pair errors on k-bit + DDP, drop to adapter: lora and load_in_4bit: false (bf16 LoRA) and keep the same torchrun command.

P2P support not detected. Axolotl set NCCL_P2P_DISABLE=1 on this PCIe pair. Training still used both cards. Do not force P2P on cards that are not bridged.

Only GPU 0 is busy. You launched without --launcher torchrun -- --nproc_per_node=2, or CUDA_VISIBLE_DEVICES=0 is still set. The 1-GPU baseline is supposed to idle GPU 1.

Gated Llama 401. This guide uses ungated Qwen2.5 Instruct. Llama 3.x needs HF_TOKEN in the environment before axolotl train.

Out of memory on 7B–8B. Lower micro_batch_size, raise gradient_accumulation_steps, shorten sequence_len, keep QLoRA, or move to 48 GB / 80 GB / 2× 80 GB. This 0.5B run is not an OOM test.

DeepSpeed / FSDP flags. Leave them out of this smoke. axolotl train defaults to DDP when neither deepspeed: nor fsdp_config: is set.

Fine-Tune LLMs with Axolotl on Multi-GPU VMs

Launch NVIDIA H100 instances for Axolotl LoRA and QLoRA fine-tuning. Get 80GB HBM, two-GPU DDP, and per-second billing.

Think it. Build it. Scale it.

Quick Setup Reference

# 1. Launch gpu_2x_h100, image 184
# 2. Verify two GPUs
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'nvidia-smi -L'

# 3. venv + Axolotl (uv, CUDA 13)
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
sudo apt-get update -qq && sudo apt-get install -y python3.12-venv curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH" UV_TORCH_BACKEND=cu130
uv venv --python 3.12 ~/axolotl_env
source ~/axolotl_env/bin/activate
uv pip install torch==2.12.1 torchvision
uv pip install --no-build-isolation axolotl
EOF

# 4. 1-GPU baseline
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
  'source ~/axolotl_env/bin/activate && cd ~/axolotl-run && CUDA_VISIBLE_DEVICES=0 axolotl train qlora.yml --launcher python'

# 5. 2-GPU
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
  'source ~/axolotl_env/bin/activate && cd ~/axolotl-run && axolotl train qlora-2gpu.yml --launcher torchrun -- --nproc_per_node=2'

# 6. Confirm adapter files
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
  'ls -l ~/axolotl-run/outputs/qlora-2gpu/adapter_*.json ~/axolotl-run/outputs/qlora-2gpu/*.safetensors'

Frequently Asked Questions

01What did this guide actually run?

gpu_2x_h100, image 184, Axolotl 0.18.0, Qwen/Qwen2.5-0.5B-Instruct QLoRA, yahma/alpaca-cleaned (1,024 of 51,760 rows), 60 SFT steps. 1-GPU wall 75.75 s, peak 6335 MiB on GPU 0. 2-GPU torchrun wall 81.71 s, peak 6707 MiB on both cards. Adapter 35.2 MB on disk.

02Is this faster than Unsloth?

Not measured here. Use Fine-Tune LLMs Faster with Unsloth on GPU Cloud for Unsloth. This post is Axolotl YAML + multi-GPU.

03Single GPU or multi-GPU?

7B–8B QLoRA: start on 1× L40 (48 GB). Two GPUs help when the model or sequence length does not fit, or when you want DDP throughput on a large dataset. 70B-class QLoRA: 2× H100 (this SKU). This 0.5B smoke is not a reason to rent two 80 GB cards.

04Should I merge the adapter into the base model?

Not for the smoke. Keep adapter_config.json + adapter_model.safetensors. axolotl merge-lora config.yml when you need a single Hugging Face folder for a server that does not load adapters.

05Do I need DeepSpeed?

Not for this DDP smoke. Axolotl can point deepspeed: at a ZeRO JSON for larger full fine-tunes. That is a later post.

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_2x_h100. Use on-demand if a long epoch cannot restart.

Recipe tested on August 25, 2026.