All articles

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 init
A 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 1
On 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-ssh
With 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.pt
89.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 . -d
Load 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 ps
lium 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
Browse GPUs