Train a PyTorch model on a Lium GPU
Rent a GPU on Lium, train ResNet-18 on CIFAR-10 in the background, follow the log, copy the checkpoint back and remove the pod. About ten cents on an RTX A6000.
Lium team5 min read
pytorch
training
resnet
cifar-10
rtx-a6000
You don't need a GPU on your desk to train a model. Rent one on Lium for as long as the job runs, start the training from your laptop, and copy the result back when it's done.
In this guide we train ResNet-18 on CIFAR-10 for ten epochs on a single GPU. Every command and number below comes from our test run on 15 October 2026: a 1x RTX A6000 at $0.42 per hour, ready 15 seconds after we asked for it, 89.6% test accuracy after 94 seconds of training.
1. Pick a GPU
Install the CLI and sign in once.
lium init opens the browser and saves your API key and SSH key.bash
pip install lium.io
lium initA small vision model fits on any card with 16 GB or more.
lium ls lists free nodes, cheapest per GPU-hour first; --count 1 keeps only single-GPU rentals.bash
lium ls --min-vram 16 --count 1On our run the cheapest rows were RTX A6000 (48 GB) nodes at $0.42 per GPU-hour, so that's what we used. Check the current A6000 price before you start; prices and availability change through the day.
2. Rent the pod
--gpu A6000 rents the cheapest matching node. --ttl 2h removes the pod two hours after the rent even if you forget it, -y skips the confirmation and --no-ssh returns to your shell instead of opening a session.bash
lium up --gpu A6000 --name resnet --ttl 2h -y --no-sshWith no
--image or template given, the pod starts from Lium's default PyTorch template: PyTorch 2.12 with CUDA 13.0, root access and its own Docker daemon. Our pod was ready 15 seconds after the command started. You pay per second from the rent request, startup included, until you remove it.3. Upload the training script
Save this as
train.py on your laptop. It loads CIFAR-10 from the Hugging Face Hub, adapts ResNet-18 to 32x32 images, and trains with SGD, a one-cycle learning-rate schedule and bfloat16 autocast. It prints test accuracy after each epoch and saves the weights at the end.python
import time, torch, torch.nn as nn, torchvision, torchvision.transforms as T
from datasets import load_dataset
device = "cuda"
mean, std = (0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)
train_tf = T.Compose([T.RandomCrop(32, padding=4), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean, std)])
test_tf = T.Compose([T.ToTensor(), T.Normalize(mean, std)])
# CIFAR-10 from the Hugging Face Hub: 50,000 training and 10,000 test images
cifar = load_dataset("uoft-cs/cifar10")
class CIFAR(torch.utils.data.Dataset):
def __init__(self, split, tf):
self.ds, self.tf = cifar[split], tf
def __len__(self):
return len(self.ds)
def __getitem__(self, i):
row = self.ds[i]
return self.tf(row["img"]), row["label"]
train_set, test_set = CIFAR("train", train_tf), CIFAR("test", test_tf)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=256, shuffle=True, num_workers=4, pin_memory=True)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=512, num_workers=4)
# ResNet-18 with a 3x3 stem and no max-pool, the usual change for 32x32 images
model = torchvision.models.resnet18(num_classes=10)
model.conv1 = nn.Conv2d(3, 64, 3, 1, 1, bias=False)
model.maxpool = nn.Identity()
model = model.to(device).to(memory_format=torch.channels_last)
EPOCHS = 10
opt = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4, nesterov=True)
sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=0.1, epochs=EPOCHS, steps_per_epoch=len(train_loader))
loss_fn = nn.CrossEntropyLoss()
start = time.time()
for epoch in range(EPOCHS):
model.train()
for x, y in train_loader:
x, y = x.to(device, non_blocking=True).to(memory_format=torch.channels_last), y.to(device, non_blocking=True)
with torch.autocast("cuda", dtype=torch.bfloat16):
loss = loss_fn(model(x), y)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()
sched.step()
model.eval()
correct = 0
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
for x, y in test_loader:
correct += (model(x.to(device).to(memory_format=torch.channels_last)).argmax(1).cpu() == y).sum().item()
print(f"epoch {epoch + 1}/{EPOCHS} loss {loss.item():.3f} test acc {correct / len(test_set):.2%} {time.time() - start:.0f}s", flush=True)
torch.save(model.state_dict(), "resnet18_cifar10.pt")
print("saved resnet18_cifar10.pt", flush=True)We load the dataset from the Hugging Face Hub because torchvision's built-in
CIFAR10(download=True) fetches from the dataset's original host, which served it at about 65 kB/s during our test. The Hub copy downloaded in seconds.Copy the script to the pod and install the one package the template doesn't include:
bash
lium scp resnet ./train.py /workspace/train.py
lium exec resnet "pip install -q datasets"4. Start training in the background
lium exec --detach starts the command with nohup and setsid on the pod and returns right away, so the job keeps running if your laptop sleeps or your connection drops. --log names the file its output goes to.bash
lium exec resnet -d --log /workspace/train.log "cd /workspace && python train.py"It prints the PID and the log path:
text
Started on eager-raven-d2: PID 1486, log /workspace/train.log
follow with: lium exec eager-raven-d2 "tail -n 200 /workspace/train.log"5. Follow the log
Check progress whenever you like:
bash
lium exec resnet "tail -n 20 /workspace/train.log"On the A6000 each epoch took about 9.5 seconds. This is the output of our run:
text
epoch 1/10 loss 1.288 test acc 33.95% 10s
epoch 2/10 loss 0.959 test acc 47.75% 19s
epoch 3/10 loss 1.006 test acc 56.26% 28s
epoch 4/10 loss 0.582 test acc 70.99% 38s
epoch 5/10 loss 0.606 test acc 75.07% 48s
epoch 6/10 loss 0.472 test acc 77.29% 57s
epoch 7/10 loss 0.340 test acc 83.62% 67s
epoch 8/10 loss 0.325 test acc 86.71% 76s
epoch 9/10 loss 0.217 test acc 88.94% 85s
epoch 10/10 loss 0.182 test acc 89.64% 94s
saved resnet18_cifar10.pt89.6% test accuracy in ten epochs and 94 seconds. Raise
EPOCHS for more; the one-cycle schedule adapts to whatever number you set.6. Copy the checkpoint back
lium scp -d downloads from the pod. The 45 MB checkpoint took 6 seconds.bash
lium scp resnet /workspace/resnet18_cifar10.pt . -dLoad it on any machine with the same model definition (the
resnet18 lines with the 3x3 stem) and model.load_state_dict(torch.load("resnet18_cifar10.pt")).Clean up
Remove the pod as soon as you have the checkpoint. Billing stops when it's gone.
bash
lium rm resnet -y
lium pslium rm reports the pod's uptime and cost. Our pod was up 13 minutes and the billing ledger charged $0.09. That includes about ten minutes we lost to the slow CIFAR-10 download above before switching to the Hub copy; the run as written here needs about five.Why Lium for this
Lium bills by the second with no minimum, and only while the pod exists, so a two-minute training job costs cents rather than a rounded-up hour. The
--ttl flag sets a removal time the moment the pod is created, so a forgotten experiment stops billing on its own. And the pod gives you root plus your own Docker daemon, which makes it closer to a VM than a plain container: install packages, run containers inside it, and keep your usual workflow.Run it on Lium
See RTX-A6000 pricing and availability before you start.