A focused reference distilled from the full Mila docs for Masters/PhD students. Covers: cluster access, SLURM, storage, GPU/partition choice, Python envs with
uv, experiment tracking, distributed training, checkpointing, HPO, containers, and FAQ.
- Ask your supervisor for a Mila invite → obtain
@mila.quebecaccount. - Complete the IT Onboarding Training (on the Mila intranet) and submit the quiz. IT then emails cluster connection details (username, etc.) within ~48h.
- IT activates Multi-Factor Authentication (MFA) and sends a one-time registration token.
Windows users: install WSL2 (wsl --install Ubuntu) and run everything inside Ubuntu.
Cluster access requires two factors: your SSH key + a second factor (TOTP, push, or email).
- Go to https://mfa.mila.quebec.
- Username = cluster username (not your
@mila.quebecemail). - Password = the one-time registration token (not your Mila password).
- Immediately add a TOTP token — scan the QR code into an authenticator app (privacyIDEA, Authy, Google Authenticator). After this first session, the portal only accepts TOTP codes; if you skip this step you need a new registration token from IT support.
Enroll two factors if you can (Push + TOTP) for redundancy.
Install uv locally and then milatools, which wires up ~/.ssh/config:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install --upgrade milatools
mila init # prompts for your cluster username, sets up SSH keysThis creates entries mila, mila-cpu, mila1..mila5, cn-???? in your SSH config.
It also uploads your public key to ~/.ssh/authorized_keys on the cluster.
Manual equivalent (if you don't want milatools) — ~/.ssh/config:
Host mila login.server.mila.quebec
Hostname login.server.mila.quebec
Host cn-????
Hostname %h.server.mila.quebec
Match host !*login.server.mila.quebec,*.server.mila.quebec
ProxyJump mila
Match host *login.server.mila.quebec
Port 2222
ServerAliveInterval 120
Match host *.server.mila.quebec
PreferredAuthentications publickey,keyboard-interactive
AddKeysToAgent yes
User YOUR_MILA_USERNAMEssh mila # land on a login node (you'll be prompted for OTP)
scp file.zip mila:scratch/uploaded.zip
rsync -avz mila:path/ local-path/Never run anything heavy on a login node — it's shared with everyone.
Allowed: editing with vim/nano, cp/mv, tmux, submitting jobs, light scripts that mostly sleep.
Not allowed: training scripts, building wheels like flash-attn from source, anything CPU/RAM-heavy.
If you need a small interactive environment for building envs or editing:
ssh mila-cpu # auto-allocates a CPU compute node, reuses the same job across connectionsOnce you have a job on cn-XXXX, from a login node: ssh cn-XXXX. From your laptop: ssh -J mila USERNAME@cn-XXXX (the -J jump is required because compute nodes are firewalled).
uv— fast Python package manager; drop-in replacement for pip/virtualenv.milatools/milaCLI — SSH config helper +mila codefor VSCode on compute nodes.- MFA — required for every SSH login. If the OTP prompt doesn't appear, MFA isn't set up.
Mila and DRAC both use SLURM. You submit jobs from a login node; they run on compute nodes.
| Command | Purpose |
|---|---|
sbatch job.sh |
Submit a batch job (non-interactive). |
salloc [opts] |
Request resources and drop into an interactive shell. |
srun [opts] cmd |
Run a command inside an allocation (also used inside batch scripts). |
squeue --me (or -u $USER) |
Show your jobs. |
scancel <JOBID> |
Cancel a job. scancel -u $USER cancels all yours. scancel -t PD cancels pending. |
scontrol show job <JOBID> |
Detailed info on a running job. |
sinfo |
Nodes and partitions. |
savail |
(Mila-custom) List available GPUs. |
sacct -u $USER --start=YYYY-MM-DD |
Job history. |
Max 1000 submitted jobs per user at a time.
#!/bin/bash
#SBATCH --ntasks=1 # one task (one process)
#SBATCH --cpus-per-task=4 # 4 CPU cores for that task
#SBATCH --gpus-per-task=l40s:1 # one L40S GPU; also: rtx8000:1, a100:1, or :1 for any
#SBATCH --mem-per-gpu=16G # or --mem=16G for total
#SBATCH --time=00:15:00 # HH:MM:SS; after this the job is killed
#SBATCH --partition=unkillable # see partition table
#SBATCH --output=slurm-%j.out # %j = job id
#SBATCH --requeue # allow requeue after preemption
#SBATCH --signal=B:TERM@300 # send SIGTERM 5 min before time limitInside the script, run training via srun:
srun uv run python main.pyPriority order: unkillable > main > long. Higher-priority jobs can preempt lower ones;
preempted jobs are killed immediately and re-queued on the same partition.
main jobs never preempt each other, regardless of fair-share usage.
| Partition | Max resources | Max time | Notes |
|---|---|---|---|
unkillable |
6 CPUs, 32 GB, 1 GPU | 2 days | Cannot be preempted. |
unkillable-cpu |
2 CPUs, 16 GB | 2 days | CPU-only. |
short-unkillable |
1000 GB, 4 GPUs | 3 hours | 4-GPU nodes only (cn-g, cn-l, cn-n); H100s live here. |
main |
8 CPUs, 48 GB, 2 GPUs | 5 days | Default priority. |
main-cpu |
8 CPUs, 64 GB | 5 days | CPU-only. |
long |
unlimited | 7 days | Preemptible by main/unkillable. |
long-cpu |
unlimited | 7 days | CPU-only. |
A CPU-only request on unkillable/main/long is auto-translated to the *-cpu variant.
H100s are only available on short-unkillable (on cn-n nodes, 8 GPUs/node but only 4 bookable per job).
--gpus-per-task=l40s:1 # specific model
--gpus-per-task=1 # any GPU model
--gres=gpu:a100:1 # same idea, older syntax
--gres=gpu:48gb:1 # at least 48 GB memory
--constraint="dgx&ere" # DGX A100 nodes
--constraint=nvlink # NVLink-connected GPUsAvailable feature tags: turing, volta, ampere, hopper, lovelace, nvlink, dgx, GPU-memory tags like 12gb/32gb/40gb/48gb/80gb.
| Nodes | GPU | Mem (GB) | #GPUs | Features |
|---|---|---|---|---|
| cn-a | RTX8000 | 48 | 8 | turing,48gb |
| cn-b | V100 | 32 | 8 | volta,nvlink,32gb |
| cn-c | RTX8000 | 48 | 8 | turing,48gb |
| cn-g | A100 | 80 | 4 | ampere,nvlink,80gb |
| cn-i001 | A100 | 80 | 4 | ampere,80gb |
| cn-j001 | A6000 | 48 | 8 | ampere,48gb |
| cn-k | A100 | 40 | 4 | ampere,nvlink,40gb |
| cn-l | L40S | 48 | 4 | lovelace,48gb |
| cn-n | H100 | 80 | 8 | hopper,nvlink,80gb |
| cn-d | A100 (DGX) | 40/80 | 8 | ampere,nvlink,dgx |
| cn-e | V100 (DGX) | 32 | 8 | volta,nvlink,dgx,32gb |
| cn-f | CPU (rome) | — | — | — |
| cn-h | CPU (milan) | — | — | — |
| cn-m | CPU (sapphire) | — | — | — |
DGX A100 40 GB: --gres=gpu:a100:N --constraint="dgx&ere".
DGX A100 80 GB: --gres=gpu:a100l:N --constraint="dgx&ere".
salloc # CPU-only shell, default resources
salloc --gres=gpu:1 -c 2 --mem=12G # one GPU, 2 CPUs, 12G RAM
salloc --gres=gpu:1 -c 4 --mem=10G -t 12:00:00 --partition=unkillable| Path | Perf | Purpose | Quota | Backup | Auto-cleanup |
|---|---|---|---|---|---|
/network/datasets/ |
high | Curated read-only datasets | — | — | — |
/network/weights/ |
high | Curated read-only model weights | — | — | — |
$HOME (/home/mila/<u>/<username>) |
low | Code, small libs | 100 GB / 1M files | daily | no |
$SCRATCH (/network/scratch/<u>/<username>) |
high | Temporary results, processed datasets | 5 TB / unlimited | no | 90 days |
$SLURM_TMPDIR |
highest | Per-job local disk | — | no | at job end |
/network/projects/<group>/ |
fair | Long-term shared | 1 TB / 1M | daily | no |
$ARCHIVE (/network/archive/<u>/<username>) |
low | Long-term personal, login+CPU nodes only | 5 TB soft / 5.1 TB hard | no | no |
Check quotas: disk-quota (on login nodes). df -h $ARCHIVE for archive.
The core data-staging pattern: copy your dataset to $SLURM_TMPDIR at the start of every job.
Network storage is shared and slower; $SLURM_TMPDIR is node-local SSD.
mkdir -p $SLURM_TMPDIR/data
cp /network/datasets/cifar10/cifar-10-python.tar.gz $SLURM_TMPDIR/data/
# Or: tar -xf /network/datasets/foo.tar -C $SLURM_TMPDIR/data/
# Or: unzip /network/datasets/foo.zip -d $SLURM_TMPDIR/data/For multi-node jobs, stage on one task per node:
srun --ntasks=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 bash -c \
'mkdir -p $SLURM_TMPDIR/data && cp /network/datasets/... $SLURM_TMPDIR/data/'/network/datasets/— public curated datasets. Browse at https://datasets.server.mila.quebec./network/datasets/restricted/— access-gated; submit a support ticket citing the access group.- Datasets are mirrored to DRAC at
~/projects/rrg-bengioy-ad/data/curated/.
To share $SCRATCH/X/Y/Z/ with $USER2 (safer than chmod 770):
setfacl -Rdm user:${USER}:rwx $SCRATCH/X/Y/Z/ # inheritable self-access
setfacl -Rdm user:${USER2}:rwx $SCRATCH/X/Y/Z/ # inheritable other-access (future files)
setfacl -Rm user:${USER2}:rwx $SCRATCH/X/Y/Z/ # existing files
setfacl -m user:${USER2}:x $SCRATCH/X/Y/ # traversal (non-recursive!)
setfacl -m user:${USER2}:x $SCRATCH/X/
setfacl -m user:${USER2}:x $SCRATCHCheck with getfacl <path>.
uv consolidates pip, virtualenv, pip-tools into a single fast tool.
curl -LsSf https://astral.sh/uv/install.sh | sh # run on both laptop and login nodeIf uv isn't found, reopen the shell (PATH update).
uv init my-project --python=3.12 # creates pyproject.toml, .python-version, hello.py
cd my-project
uv add torch "numpy>=2.0" # adds deps, updates pyproject.toml, syncs .venv
uv add --dev pytest # dev-only dep
uv remove torch
uv sync # reproduce .venv from uv.lock
uv cache prune # free disk (stale cache only)
uv cache clean # wipe cache entirelyCommit uv.lock to version control — it pins exact transitive versions for reproducibility.
uv run python train.py # syncs env, runs in project venv
uv run pytestNo need to manually activate .venv; uv run does it each time.
uv add --script experiment.py numpy matplotlib
uv run --script experiment.pyDeclares deps in a # /// script block at the top of the file; uv run creates a temp env, runs, discards.
uv tool install --upgrade wandb
uv tool install --upgrade milatools
uv tool list
uv tool upgrade --allUse uv tool install for commands you want globally (wandb, mila). Use uv add for libraries your code imports.
#!/bin/bash
#SBATCH --gpus-per-task=l40s:1
#SBATCH --cpus-per-task=4
#SBATCH --mem=16G
#SBATCH --time=01:00:00
srun uv run python train.pyUseful flags: uv run --offline (no network — required on DRAC compute nodes), uv run --locked (fail if lockfile out of date, enforces reproducibility).
module load python/3.10
python -m venv $HOME/env
source $HOME/env/bin/activate
pip install torch torchvisionModules: module avail, module load python/3.12, module spider <name>.
Mila has a team WandB organization — request access at it-support@mila.quebec.
Go to https://wandb.ai, enter your @mila.quebec address, click Log in — SSO will redirect to Mila's IDP.
Choose Professional at account creation. If you already had a personal WandB account, add @mila.quebec as primary email first to avoid duplicates.
uv tool install --upgrade wandb
wandb login # paste API key from wandb.ai/authorize; stored in ~/.netrcimport os, wandb
wandb.init(
project="my-project",
name=os.environ.get("SLURM_JOB_ID"),
id=os.environ.get("SLURM_JOB_ID"), # lets us resume the same run
group=os.environ.get("SLURM_ARRAY_JOB_ID"), # groups array-task runs in the UI
resume="allow",
tags=["resnet18", "example"],
config=vars(args)
| {f"env/{k}": v for k, v in os.environ.items() if k.startswith("SLURM")},
)
wandb.log({"train/loss": loss, "train/accuracy": acc})
# once per epoch:
wandb.log({"val/loss": vl, "val/accuracy": va, "epoch": epoch})
wandb.finish() # explicit is safer in SLURM jobsPrefix metric names with train/ and val/ — the UI groups matching prefixes into the same chart.
export WANDB_MODE=offline
srun uv run --offline python main.py
# after the job:
wandb sync --sync-allOn DRAC, alternatively: module load httpproxy before srun, then log online.
Caution on DRAC: httpproxy + WandB artifacts is buggy — the logger hangs until the job times out. Prefer offline mode.
WandB auto-logs GPU/CPU/memory under the System tab. A sustained ~100% GPU utilization → compute-bound (good).
Low or oscillating utilization → I/O-bound: fix by increasing DataLoader(num_workers=...), pin_memory=True, or staging data to $SLURM_TMPDIR.
job.sh:
#!/bin/bash
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --gpus-per-task=l40s:1
#SBATCH --mem-per-gpu=16G
#SBATCH --time=00:15:00
set -e
echo "Date: $(date)"; echo "Hostname: $(hostname)"
# Stage dataset to local SSD
mkdir -p $SLURM_TMPDIR/data
cp /network/datasets/cifar10/cifar-10-python.tar.gz $SLURM_TMPDIR/data/
srun uv run python main.pymain.py (skeleton):
import argparse, logging, os, random, sys
from pathlib import Path
import numpy as np
import torch
from torch import Tensor, nn
from torch.nn import functional as F
from torch.utils.data import DataLoader, random_split
from torchvision import transforms
from torchvision.datasets import CIFAR10
from torchvision.models import resnet18
from tqdm import tqdm
def main():
p = argparse.ArgumentParser()
p.add_argument("--epochs", type=int, default=10)
p.add_argument("--learning-rate", type=float, default=5e-4)
p.add_argument("--weight-decay", type=float, default=1e-4)
p.add_argument("--batch-size", type=int, default=128)
p.add_argument("--seed", type=int, default=42)
args = p.parse_args()
random.seed(args.seed); np.random.seed(args.seed)
torch.random.manual_seed(args.seed); torch.cuda.manual_seed_all(args.seed)
assert torch.cuda.is_available() and torch.cuda.device_count() > 0
device = torch.device("cuda", 0)
model = resnet18(num_classes=10).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=args.learning_rate,
weight_decay=args.weight_decay)
data_path = Path(os.environ.get("SLURM_TMPDIR", ".")) / "data"
train_ds, val_ds, test_ds = make_datasets(str(data_path))
num_workers = get_num_workers()
train_loader = DataLoader(train_ds, batch_size=args.batch_size,
num_workers=num_workers, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=args.batch_size,
num_workers=num_workers, shuffle=False)
for epoch in range(args.epochs):
model.train()
for x, y in tqdm(train_loader, desc=f"epoch {epoch}",
disable=not sys.stdout.isatty()):
x, y = x.to(device), y.to(device)
loss = F.cross_entropy(model(x), y)
opt.zero_grad(); loss.backward(); opt.step()
val_loss, val_acc = validation_loop(model, val_loader, device)
print(f"Epoch {epoch}: val_loss={val_loss:.3f} val_acc={val_acc:.2%}")
@torch.no_grad()
def validation_loop(model, loader, device):
model.eval()
total_loss, n, correct = 0.0, 0, 0
for x, y in loader:
x, y = x.to(device), y.to(device)
logits = model(x)
total_loss += F.cross_entropy(logits, y).item()
n += x.shape[0]
correct += logits.argmax(-1).eq(y).sum().item()
return total_loss, correct / n
def make_datasets(path, val_split=0.1, seed=42):
train = CIFAR10(root=path, transform=transforms.ToTensor(), download=True, train=True)
test = CIFAR10(root=path, transform=transforms.ToTensor(), download=True, train=False)
n_val = int(val_split * len(train))
train, val = random_split(train, (len(train) - n_val, n_val),
torch.Generator().manual_seed(seed))
return train, val, test
def get_num_workers():
if "SLURM_CPUS_PER_TASK" in os.environ:
return int(os.environ["SLURM_CPUS_PER_TASK"])
return len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") \
else torch.multiprocessing.cpu_count()
if __name__ == "__main__":
main()job.sh:
-#SBATCH --ntasks=1
-#SBATCH --ntasks-per-node=1
+#SBATCH --ntasks=2
+#SBATCH --ntasks-per-node=2
#SBATCH --gpus-per-task=l40s:1
+export MASTER_PORT=$(expr 10000 + $(echo -n $SLURM_JOBID | tail -c 4))
+export MASTER_ADDR="127.0.0.1"
-srun uv run python main.py
+srun --gres-flags=allow-task-sharing uv run python main.py--gres-flags=allow-task-sharing is required when using --gpus-per-task=1; without it, NCCL fails with ncclUnhandledCudaError because cgroups isolate each task's shared memory (see slurm.schedmd.com bug 17875).
Key code changes in main.py:
import torch.distributed as dist
from torch.distributed import ReduceOp
from torch.utils.data.distributed import DistributedSampler
def setup():
rank = int(os.environ["SLURM_PROCID"])
world_size = int(os.environ["SLURM_NTASKS"])
os.environ["RANK"] = str(rank)
os.environ["WORLD_SIZE"] = str(world_size)
dist.init_process_group(backend="nccl", init_method="env://",
world_size=world_size, rank=rank)
return rank, world_size
# In main():
rank, world_size = setup()
is_master = rank == 0
device = torch.device("cuda", 0) # SLURM sets CUDA_VISIBLE_DEVICES per-task
model = nn.parallel.DistributedDataParallel(model.to(device),
device_ids=[0], output_device=0)
train_sampler = DistributedSampler(train_ds, shuffle=True)
train_loader = DataLoader(train_ds, batch_size=batch_size, sampler=train_sampler,
shuffle=False, num_workers=num_workers)
for epoch in range(epochs):
train_sampler.set_epoch(epoch) # required so shuffle order varies
# ...
# Reduce metrics across ranks:
n_correct = local_n_correct.clone()
dist.reduce(n_correct, dst=0, op=ReduceOp.SUM)In make_datasets(), let only the master rank download, others wait on dist.barrier() before reading from disk.
-#SBATCH --ntasks=2
+#SBATCH --ntasks=4
#SBATCH --ntasks-per-node=2
#SBATCH --gpus-per-task=l40s:1
-mkdir -p $SLURM_TMPDIR/data && cp ... $SLURM_TMPDIR/data/
+srun --ntasks=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 bash -c \
+ 'mkdir -p $SLURM_TMPDIR/data && cp /network/datasets/... $SLURM_TMPDIR/data/'
-export MASTER_ADDR="127.0.0.1"
+export MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)In setup(): use TCP init (the env:// init method uses file locks and deadlocks under Mila's distributed FS config).
local_rank = int(os.environ["SLURM_LOCALID"]) # for data-download sync (per-node)
dist.init_process_group(
backend="nccl",
init_method=f"tcp://{os.environ['MASTER_ADDR']}:{os.environ['MASTER_PORT']}",
timeout=timedelta(seconds=60),
world_size=world_size, rank=rank,
)Use local_rank == 0 (one process per node) to decide who downloads data on each node.
#SBATCH --nodes=3
#SBATCH --ntasks=3
#SBATCH --gpus-per-node=4
#SBATCH --cpus-per-gpu=4
RDV_ADDR=$(hostname)
srun --label torchrun \
--nproc_per_node=$SLURM_GPUS_PER_NODE \
--nnodes=$SLURM_JOB_NUM_NODES \
--rdzv_id=$SLURM_JOB_ID \
--rdzv_backend=c10d \
--rdzv_endpoint=$RDV_ADDR \
training_script.pyPreemption on the long or main-grace/long-grace partitions sends signals before kill. Trap them and save state.
job.sh:
#SBATCH --requeue
#SBATCH --signal=B:TERM@300 # SIGTERM 5 min before time limit
exec srun uv run python main.py # exec so signals reach Python directlyIn main.py:
import signal, shutil, uuid, torch
from pathlib import Path
SCRATCH = Path(os.environ["SCRATCH"])
SLURM_JOBID = os.environ["SLURM_JOBID"]
CKPT_DIR = SCRATCH / "myproj" / SLURM_JOBID / "checkpoints"
CKPT_FILE = "checkpoint.pth"
def handle_preempt(signum, frame):
logger.error(f"Received {signal.Signals(signum).name}")
# Quick action; don't checkpoint mid-epoch here.
# If using wandb: wandb.mark_preempting()
signal.signal(signal.SIGTERM, handle_preempt) # before requeue
signal.signal(signal.SIGUSR1, handle_preempt) # before time limit
# Atomic save: write to temp, then os.replace
def save_checkpoint(state, is_best):
CKPT_DIR.mkdir(parents=True, exist_ok=True)
tmp = CKPT_DIR / f"{CKPT_FILE}.tmp{uuid.uuid1()}"
torch.save(state, tmp)
os.replace(tmp, CKPT_DIR / CKPT_FILE)
if is_best:
shutil.copyfile(CKPT_DIR / CKPT_FILE, CKPT_DIR / "model_best.pth")
# At the top of training, resume if possible
if (CKPT_DIR / CKPT_FILE).exists():
state = torch.load(CKPT_DIR / CKPT_FILE, weights_only=False)
start_epoch = state["epoch"] + 1
model.load_state_dict(state["model_state"])
optimizer.load_state_dict(state["optimizer_state"])
# Also restore RNG states (random, np.random, torch, torch.cuda) for reproducibilitySave the full RunState at each epoch: epoch, best_acc, model_state, optimizer_state,
random_state, numpy_random_state, torch_random_state, torch_cuda_random_state.
Check SLURM_RESTART_COUNT to detect that SLURM requeued you — if a checkpoint isn't there despite restarts, your checkpointing code isn't running before preemption.
# job.sh
srun uv run python main.py "$@"
# Submit:
sbatch job.sh --learning-rate 0.1
sbatch job.sh --learning-rate 0.5 --weight-decay 1e-3sbatch --array=1-100 --gres=gpu:1 --cpus-per-gpu=2 --mem-per-gpu=16G job.sh train.py$SLURM_ARRAY_JOB_ID, $SLURM_ARRAY_TASK_ID are exported automatically. Use them to pick hyperparameters, seed, and (with WandB) group=$SLURM_ARRAY_JOB_ID.
Orion stores trials in a pickleddb on shared storage. Use it with job arrays.
export ORION_CONFIG=$SLURM_TMPDIR/orion-config.yml
cat > $ORION_CONFIG <<EOM
experiment:
name: orion-example
algorithms:
tpe: { seed: null, n_initial_points: 5 }
max_broken: 10
max_trials: 10
storage:
database:
host: $SCRATCH/orion.pkl
type: pickleddb
EOM
srun uv run orion hunt --config $ORION_CONFIG python main.py \
--learning-rate~'loguniform(1e-5, 1.0)'In your script, report the objective at the end:
from orion.client import report_objective
report_objective(1 - val_accuracy) # Orion minimizes, so convert accuracyShare a GPU between N tasks:
#SBATCH --ntasks=2 --gres=gpu:1 --cpus-per-task=4 --mem=18G
srun --label --output=out_%t.out python script "$@"Or multi-prog:
srun --label --multi-prog config.conf
# config.conf:
# 0 python script firstarg
# 1 python script secondargOne task per GPU on a full node (4 GPUs):
#SBATCH --nodes=1 --gres=gpu:4 --ntasks-per-node=4 --cpus-per-task=6
srun --gres=gpu:1 --ntasks=1 --mem=30G --exclusive python script args1 &
srun --gres=gpu:1 --ntasks=1 --mem=30G --exclusive python script args2 &
srun --gres=gpu:1 --ntasks=1 --mem=30G --exclusive python script args3 &
srun --gres=gpu:1 --ntasks=1 --mem=30G --exclusive python script args4 &
wait--exclusive binds each srun step to distinct CPUs.
Use mila code, which allocates a compute node and opens VSCode connected to it.
mila code CODE/my_project --alloc --gres=gpu:1 --cpus-per-task=2 --mem=16G --time=01:00:00Everything after --alloc is passed to salloc. For CPU-only editing: mila code dir --alloc --cpus-per-task=2 --mem=8G.
Do not connect plain VSCode Remote-SSH to login.server.mila.quebec — it runs a non-trivial server process and will overload the login nodes. Use mila-cpu (from mila init) instead.
- VSCode stuck on "Opening Remote...": enable
remote.SSH.showLoginTerminal— the OTP prompt is hidden. - "Cannot reconnect" lockfile errors when connecting to multiple nodes: set
"remote.SSH.lockfilesInTmp": trueinsettings.json—~/.vscode-serveris shared across compute nodes. - Debugger timeouts:
export DEBUGPY_PROCESS_SPAWN_TIMEOUT=500in~/.bashrc.
Cmd/Ctrl+Shift+P → "Python: Select interpreter" → paste the path printed by which python (from an activated env).
Available at https://jupyterhub.server.mila.quebec (Google OAuth with @mila.quebec).
Launches a JupyterLab session as a SLURM job automatically.
Close sessions via Hub → Control Panel → Stop my server — closing the window leaves the SLURM job running.
JupyterLab cannot navigate above $HOME, so $SLURM_TMPDIR and /network/datasets are invisible. Create symlinks:
ln -s /network/datasets $HOME
ln -sf $SLURM_TMPDIR $HOME # needs to be redone each sessionDocker doesn't run on the cluster (security). Use Podman inside SLURM jobs — compatible with Docker CLI.
podman run --mount type=bind,source=$SCRATCH/exp,destination=/data/exp bash touch /data/exp/file
# With GPUs:
podman run --device nvidia.com/gpu=all nvidia/cuda:11.6.1-base-ubuntu20.04 nvidia-smi
# Or specific GPU indices:
podman run --device nvidia.com/gpu=0 ...Storage: Podman images/containers live in a job-specific location that's wiped at job end.
Bind-mount $SCRATCH or $HOME for persistent data.
Expect benign warnings like WARN[0000] "/" is not a shared mount and cannot find UID/GID.
Docker is unavailable; Singularity is standard on Alliance clusters. Build images elsewhere
(build is CPU-heavy — don't do it on login nodes). Run pre-built images from a .sif file.
# Pull a public image (on a compute node with internet, not login)
singularity pull docker://pytorch/pytorch:2.0-cuda11.7-cudnn8-runtime
# Use in a job
module load singularity # DRAC only
rsync -avz $SCRATCH/my.sif $SLURM_TMPDIR
singularity exec --nv \
-H $HOME:/home \
-B $SLURM_TMPDIR:/dataset/ \
-B $SCRATCH:/final_log/ \
$SLURM_TMPDIR/my.sif python main.py--nv enables GPU passthrough. -H sets the container home; -B src:dst binds host paths.
For larger/longer jobs than Mila allows. Main clusters and GPU types:
| Cluster | Successor to | GPU | Notes |
|---|---|---|---|
| Fir | Cedar | H100-80G | Retains Cedar's FS. |
| Nibi | Graham | H100-80G | Retains Graham's FS. |
| Rorqual | Beluga | H100-80G | No internet on compute nodes. |
| Trillium | Niagara | H100 | Mostly CPU, per-node (not per-CPU) allocation. Not generally recommended. |
| Narval | — | A100-40G | Oldest. Good for sub-H100 workloads. |
| TamIA | — | H100/H200 | PAICE AI cluster. Uses --account=aip-${PI} (not rrg-). Per-node allocation. |
- Register at https://ccdb.alliancecan.ca (password ≥ 8 chars, mixed case, digits, special).
- Apply for a
roleat https://ccdb.alliancecan.ca/me/add_role — you need your sponsor's CCRI (ask supervisor). Apply once per allocation (rrg-bengioy-ad,def-<supervisor>). - Wait for the sponsor to accept. Accounts must be renewed annually.
You must specify an --account:
sbatch --time=1:00:00 --account=rrg-bengioy-ad --gres=gpu:1 job.sh
salloc --time=1:00:00 --account=rrg-bengioy-ad --gres=gpu:1| Path | Purpose |
|---|---|
$HOME (/home/<user>) |
Code, libraries (read once). |
$HOME/projects/rrg-bengioy-ad |
Compressed raw datasets. |
$SCRATCH (/scratch/<user>) |
Processed datasets, results. Purged at ~3 months. |
$SLURM_TMPDIR |
Per-job local disk. |
module load StdEnv/2023 python/3.12
virtualenv --no-download $SLURM_TMPDIR/env
source $SLURM_TMPDIR/env/bin/activate
pip install --no-index torch torchvision # use precompiled wheels
# Copy dataset
unzip $SCRATCH/dataset.zip -d $SLURM_TMPDIR
python main.py --path $SLURM_TMPDIR --data_path $SLURM_TMPDIR
cp $SLURM_TMPDIR/results $SCRATCHAvailable precompiled wheels: avail_wheels <name>.
Compute nodes on Narval/Rorqual/TamIA have no internet. Options for wandb/comet:
module load httpproxyor run offline and wandb sync --sync-all from the login node afterwards.
DRAC measures allocation in Reference GPU Units, combining FP32/FP16 perf and memory. Rough guide: A100-40G = 4 RGU, H100-80G = 12.15 RGU.
# CLI (macOS, Linux, WSL2)
curl -fsSL https://claude.ai/install.sh | bash
claude --versionAlso available as a VS Code extension. See https://claude.com/product/claude-code.
Run claude — a browser window opens for login. Requires an Anthropic account (Pro/Team/Enterprise subscription or API-usage billing).
Skills are plugin workflows triggered as slash commands.
/plugin marketplace add mila-iqia/skills
/plugin install mila-tools@mila-skills
/reload-plugins
Available Mila skills (via mila-iqia/skills):
| Skill | Purpose |
|---|---|
/mila-account-setup |
Onboard: account, cluster access, MFA. |
/mila-local-setup |
WSL2, uv, milatools on your laptop. |
/mila-connect-cluster |
SSH + OTP + troubleshoot connectivity. |
/mila-run-jobs |
Interactive sessions + sbatch submission. |
Manage plugins with /plugin (Discover / Installed / Marketplaces / Errors tabs).
- Login vs. compute nodes. Login = shared entry point, no heavy compute. Compute = where jobs run, often with GPUs.
- Storage nodes. Dedicated machines serving
/network/*; invisible to you except via paths. - Workload manager (SLURM). You submit jobs to a queue; SLURM matches them to resources using policy (priority, fair-share, time since submission, resources asked). Partitions group nodes by purpose.
- Preemption. Soft limits → your job gets
SIGTERM(with possible grace period) when a higher-priority job needs the node. Hard limits → immediate kill. Mila's-gracepartitions give you 120 s before kill (but don't auto-requeue; callsbatchinside a trap handler if you want that). - Data vs. model parallelism. Data parallelism = N workers, each processes a shard — easiest, use first (DDP). Model parallelism = one model split across devices — needed only when a model doesn't fit on a GPU. Communication patterns: infrequent (data parallel) → tasks can be anywhere; frequent (model parallel) → colocate via
--nodes, NVLink/InfiniBand matters. - Filesystem performance. Different mounts are tuned for different I/O patterns. A bad choice (e.g. training from
$HOME) can be 100x slower. Use$SLURM_TMPDIRfor hot paths.
connection refused: the login node banned your IP after too many failed connects. Unbanned automatically after 1 hour.Permission denied (publickey)on a compute node: add your pubkey to~/.ssh/authorized_keyson the cluster or run anssh-agentthat forwards it.ssh-keygenon a login node and append toauthorized_keys.- Mila compute-node SSH fingerprints (to avoid MITM warnings):
SHA256:hGH64v72h/c0SfngAWB8WSyMj8WSAf5um3lqVsa7Cfk (ECDSA) SHA256:4Es56W5ANNMQza2sW2O056ifkl8QBvjjNjfMqpB7/1U (RSA) SHA256:gUQJw6l1lKjM1cCyennetPoQ6ST0jMhQAs/57LhfakA (ED25519)
- Job stuck with
ReqNodeNotAvail: no matching node free. Not an error — wait, orscanceland resubmit with a different GPU type (--gres=gpu:rtx8000:1). - "Invalid account or account/partition combination": your account isn't set up; file a ticket.
srun: error: --mem and --mem-per-cpu are mutually exclusive: benign;sallocadded a default. Ignore.slurmstepd: Detected 1 oom-kill event(s): your job exceeded--mem. Raise it or fix the leak.fork: retry: Resource temporarily unavailable: hit the 2000 task/PID limit. Some subprocess is spawning too many.- Cancel jobs:
scancel <JOBID>/scancel -u $USER(all) /scancel -t PD(only pending).
- Don't leave
torch.autograd.set_detect_anomaly(True)on. It stats every source file on every tensor creation, generating huge IOPS on the shared filesystem. Only enable interactively when debugging, then turn it off. You will be contacted if you abuse this. - Conda "Your installed CUDA driver is: not available" on login/CPU nodes: set
CONDA_OVERRIDE_CUDA=11.8(or your toolkit version) beforeconda create.
- Where do I put data during a job? Copy from
/network/datasets/or$SCRATCHto$SLURM_TMPDIRat job start. Save outputs you want to keep back to$SCRATCHbefore the job ends —$SLURM_TMPDIRis deleted on job end.
- Default shell is
/bin/bash. To change, file a ticket.zshis supported; other shells are not supported.
Order of escalation:
- This doc / official docs (https://docs.mila.quebec)
- On-site chatbot (the "Ask AI" button in the docs site)
- Slack
#mila-cluster(Mila's Slack) — fastest for community help - Helpdesk (IT support portal: https://it-support.mila.quebec) — tickets for account/access/SSH issues
- IDT Office Hours — in-person (Lab A) or online (mila-core calendar) for debugging + ML workflow help
Team routing:
| Issue | Contact |
|---|---|
| Account creation, SSH, VPN, hardware | IT Support (portal or #mila-cluster) |
| Job debugging, experiment optimization | IDT (#mila-cluster, office hours) |
| General admin, community | MyMila portal |
- cluster — a group of computers that run jobs together.
- login node — the entry point you SSH into; don't run heavy work here.
- compute node — where jobs actually run; GPUs live here.
- GPU — graphics processing unit; what accelerates DL training.
- SLURM — the job scheduler.
- SLURM job — a submitted unit of work, with resources and a time limit.
- SLURM job step — an individual command/program inside a job (via
srun). - batch SLURM job — a job submitted via
sbatchthat runs without you attached. - MFA — multi-factor authentication: more than one credential required.
- SSH — Secure SHell; the connection protocol.
- OTP — one-time password.
- TOTP — time-based OTP (6-digit code from an authenticator app).
- WSL — Windows Subsystem for Linux (for Windows users).
- DRAC / Alliance — Digital Research Alliance of Canada (national compute).
- DDP — Distributed Data Parallel (PyTorch's multi-GPU training).
- RGU — Reference GPU Unit (DRAC's weighted GPU allocation currency).
- HPO — hyperparameter optimization.
Derived from https://docs.mila.quebec. See the original for full context, extended examples, and the authoritative cheatsheet PDF.