GPU guide

CUDA_VISIBLE_DEVICES: choose which GPUs a process can use

CUDA_VISIBLE_DEVICES hides GPUs and renumbers the rest from 0. Why the numbers differ from nvidia-smi, UUIDs, one job per GPU, Docker, vLLM and Ollama.

Faiz Ahmed7 min read

CUDA_VISIBLE_DEVICES is a list of the GPUs a process may use. CUDA hides every GPU that is not on the list and numbers the rest from 0, in the order you wrote them, so CUDA_VISIBLE_DEVICES=1 python train.py runs the job on the second GPU and the job calls it cuda:0. It has to be set before the program starts using CUDA, which in practice means on the command line or in the environment of the service. On a multi-GPU QuantaCloud VM it is the simplest way to give each job its own GPU.

Launch a 2-GPU RTX A6000 VM

What the variable does#

NVIDIA's CUDA documentation defines every value it accepts:

ValueWhat the process sees
Not setEvery GPU
0GPU 0 only, as device 0
2,1GPU 2 as device 0 and GPU 1 as device 1. GPU 0 is hidden
GPU-8932f937The GPU whose UUID starts with these characters, as device 0
MIG-GPU-8932f937-d72c-4106-c12f-20bd9faed9f6/1/2One MIG instance, named by the GPU UUID, the GPU instance and the compute instance. Only one is supported
0,2,-1,1GPUs 0 and 2. Everything after an invalid entry is hidden
Empty stringNo GPU at all

The program never sees the hidden GPUs. CUDA's device count includes only the visible ones, so with 2,1 a call that selects device 0 gets the GPU that nvidia-smi lists as 2.

Why the numbers disagree with nvidia-smi#

The first difference is the renumbering, and it is always there. With CUDA_VISIBLE_DEVICES=2,3, PyTorch's cuda:0 and cuda:1 are the GPUs that nvidia-smi shows as 2 and 3.

The second is the order. CUDA's default, CUDA_DEVICE_ORDER=FASTEST_FIRST, numbers the GPUs from fastest to slowest by a simple heuristic, while nvidia-smi lists them by its own index, and NVIDIA does not promise that index stays the same across reboots. CUDA_DEVICE_ORDER=PCI_BUS_ID makes CUDA count in ascending PCI bus ID, which you can compare with the bus IDs nvidia-smi prints:

Terminal
nvidia-smi --query-gpu=index,name,pci.bus_id,uuid --format=csv
export CUDA_DEVICE_ORDER=PCI_BUS_ID

My rule is to set CUDA_DEVICE_ORDER=PCI_BUS_ID whenever I select GPUs by number, and to use UUIDs in anything that has to survive a reboot.

Select GPUs by UUID#

A UUID names the same GPU everywhere, and NVIDIA recommends it, or the PCI bus ID, for anything that must stay consistent. nvidia-smi -L prints one line per GPU with its UUID, and the first characters are enough when they are unique:

Terminal
nvidia-smi -L
CUDA_VISIBLE_DEVICES=GPU-8932f937 python train.py

To check which physical GPU a PyTorch process got, print the UUID of each visible device and compare it with nvidia-smi -L. PyTorch prints the UUID without the GPU- prefix, so the script adds it:

Python
import torch

for i in range(torch.cuda.device_count()):
    p = torch.cuda.get_device_properties(i)
    print(f"cuda:{i}", p.name, f"GPU-{p.uuid}")

Choose a GPU inside PyTorch#

PyTorch's own advice is to prefer CUDA_VISIBLE_DEVICES in most cases, and to use torch.device or the torch.cuda.device context manager when one process works on several GPUs:

Python
model = model.to("cuda:1")            # the second visible GPU

with torch.cuda.device(1):
    x = torch.randn(1024, 1024, device="cuda")   # also on cuda:1

To set the variable from Python, set it before import torch and before anything else that uses CUDA. PyTorch caches the device count once CUDA has initialized, and changing the variable after that has no effect.

One job per GPU on a multi-GPU VM#

The pattern for independent jobs, such as hyperparameter runs or batch inference shards, is one process per GPU, each with its own value. This loop reads the UUIDs from nvidia-smi, starts one job per GPU in the background and waits for all of them:

Terminal
mapfile -t GPUS < <(nvidia-smi --query-gpu=uuid --format=csv,noheader)
for i in "${!GPUS[@]}"; do
  CUDA_VISIBLE_DEVICES=${GPUS[$i]} python train.py --shard "$i" > "run-$i.log" 2>&1 &
done
wait

Save it as a script and run it with bash. Each job sees exactly one GPU as cuda:0, so the training script needs no device logic of its own. The logs sit on the instance's disk, and stopping a QuantaCloud instance deletes that disk, so copy them off first. One process across all the GPUs is a different setup, covered in PyTorch DDP and FSDP.

The Docker equivalents#

Docker selects GPUs with --gpus, using the index nvidia-smi shows, and the container sees only those:

DockerWhat the container gets
--gpus device=0GPU 0
--gpus '"device=0,2"'GPUs 0 and 2. The value needs both quotes because of the comma
--gpus device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2aOne GPU by its UUID from nvidia-smi -L
--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=1,2GPUs 1 and 2 through the NVIDIA runtime
--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=noneNo GPU, with the driver libraries still mounted

Inside the container only the GPUs you passed exist, so the program numbers them from 0 again. Running Docker with a GPU covers the NVIDIA Container Toolkit these flags rely on.

How vLLM, Ollama and ComfyUI choose devices#

All three accept the variable, and two have options of their own:

ToolHow to pick GPUs
vLLM 0.33.0CUDA_VISIBLE_DEVICES. vLLM's docs start the two data-parallel ranks of one deployment with CUDA_VISIBLE_DEVICES=0 and CUDA_VISIBLE_DEVICES=1, and for unrelated models they say to launch independent instances without the --data-parallel-* options. The --device-ids "2,3" option takes physical IDs or UUIDs without hiding the other GPUs, and does nothing with the Ray executor
Ollama 0.40.16CUDA_VISIBLE_DEVICES, with UUIDs preferred because the numeric order can vary. An invalid ID such as -1 forces the CPU
ComfyUI 0.45.0--cuda-device 0 or --cuda-device 0,1 sets CUDA_VISIBLE_DEVICES before PyTorch loads. --default-device 1 keeps every GPU visible and makes GPU 1 the default

Ollama usually runs as a systemd service, which does not see variables from your shell. Add the variable to the service instead:

Terminal
sudo systemctl edit ollama.service
# add under [Service]:  Environment="CUDA_VISIBLE_DEVICES=GPU-8932f937"
sudo systemctl daemon-reload
sudo systemctl restart ollama

For vLLM, two servers on two GPUs need two ports, and deploying vLLM with Docker has the full command.

Mistakes that select the wrong GPU#

Most of these fail silently, so check the UUIDs whenever a job lands somewhere unexpected:

MistakeWhat happensFix
Setting it in Python after CUDA has startedNothing changes, and the process keeps every GPUSet it in the shell, or at the top of the script before import torch
An empty value, such as CUDA_VISIBLE_DEVICES=$GPU with GPU unsetNo GPU is visible, and torch.cuda.is_available() returns FalsePrint the value with echo "[$CUDA_VISIBLE_DEVICES]"
A space after =, as in CUDA_VISIBLE_DEVICES= 1 python train.pyThe shell runs 1 as a command: 1: command not foundNo spaces around =
A space after a comma, as in CUDA_VISIBLE_DEVICES=0, 1 python train.pyThe same error, because the shell splits the value at the spaceWrite 0,1
export CUDA_VISIBLE_DEVICES = 1Bash prints not a valid identifier and leaves the variable as it was. If it was unset, the job sees every GPUexport CUDA_VISIBLE_DEVICES=1
An invalid entry such as -1 in the middle of the listEvery GPU after it is hiddenUse only valid indices or UUIDs
Selecting with nvidia-smi -iIt only chooses which GPU nvidia-smi reports on, and programs still see every GPUUse the variable

The nvidia-smi commands guide has more on reading indices, UUIDs and bus IDs.


My rule: one GPU per process with CUDA_VISIBLE_DEVICES set on the command line, UUIDs in scripts, and CUDA_DEVICE_ORDER=PCI_BUS_ID whenever I use numbers. It works the same on a 2-GPU VM as on an 8-GPU one, so start with two GPUs and move up when the catalog lists the larger configuration you need, or pick another GPU from the catalog.

Launch a multi-GPU VM

Keep building

Choose your next step.