my lab work is deep learning on echocardiogram videos. running the model on my macbook was way too slow, so everything moved to Gilbreth.
this is a quick reference for it.
what is gilbreth
Gilbreth: one of Purdue RCAC’s community clusters, the GPU one
you don’t own the machine. you ask a scheduler for resources, and it gives them to you when there’s room.
SLURM: Simple Linux Utility for Resource Management. the scheduler.
mental model:
- login node — what you ssh into. shared by everyone. for editing files, git, and submitting jobs. that’s it
- compute node — where the gpus are. you never ssh here directly, you only get here through slurm
‼️ do not run your training script on the login node. it gets killed and you’re hogging a machine ~100 other people are typing on
getting in
ssh <username>@gilbreth.rcac.purdue.edu
wants password + 2 factor every single time, which gets old fast.
add your public key so it stops asking:
ssh-copy-id <username>@gilbreth.rcac.purdue.edu
then in ~/.ssh/config on my laptop:
Host gilbreth
HostName gilbreth.rcac.purdue.edu
User <username>
ForwardAgent yes
now it’s just ssh gilbreth
agent forwarding: lets the server use my ssh key to connect somewhere else, without ever copying my private key onto the server
that’s the thing that makes git clone [email protected]:... work while i’m on gilbreth. the cluster borrows my laptop’s key to talk to github.
where files go
three storage spots and they are NOT interchangeable. this confused me for a while.
| location | what it’s for | notes |
|---|---|---|
/home/<username> | your code, configs, envs | small quota (25GB for me) |
/scratch/gilbreth/<username> | big intermediate outputs | huge, but purged periodically. not backed up |
/depot/<lab-account> | shared lab data | whole lab shares one quota |
check what you’ve used:
myquota
Type Location Size Limit Use
================================================
home <username> 17.1GB 25.0GB 68.6%
scratch <username> 4.0KB 200.0TB 0.0%
depot <lab> 3.9TB 4.0TB 100.0%
depot at 100% is how my inference run died halfway through 💀
find what’s eating it:
du -h --max-depth=1 | sort -h
du -sh /path/to/dir/* 2>/dev/null | sort -rh | head -20
-hhuman readable (G, M, K instead of blocks)-ssummarize, just the total per argsort -rhreverse sort, human-numeric aware2>/dev/nullthrow away permission-denied noise
easy wins:
rm -rf ~/.cache/pip
lesson learned: big model outputs belong in /scratch, not /depot. depot is shared with the whole lab, so filling it blocks everyone, not just me.
environment
gilbreth uses environment modules, so nothing is on your PATH until you ask for it
module load conda
conda activate <env-name>
a plain venv works too:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
what bit me: my laptop’s requirements.txt was full of mac-only pins and would not resolve. i rewrote a much simpler one with only what the script actually imports.
‼️ module load has to go inside your job script too, not just in your terminal. the compute node starts with a fresh shell and doesn’t inherit anything
interactive job (srun)
for when you want to watch it run, or you’re debugging
srun --cpus-per-task=16 --mem=32G --time=1:00:00 \
-A <your-account> -p a100-40gb --gres=gpu:1 \
python prepare_dataset.py --npz-dir ... --out-dir ...
what each flag means:
-A/--account— which allocation gets billed. your lab’s queue-p/--partition— which pool of machines. picks your gpu type (a100-40gb,a10, …)--gres=gpu:1— generic resource. this is the line that actually gets you a gpu. forget it and you get a cpu-only node and wonder why everything is slow--cpus-per-task— cpu cores. matters more than you’d think, dataloader workers live here--mem— RAM (not vram)--time— walltime limit. job gets killed at this mark, so pad it
a preprocessing step that was crawling on the login node finished almost immediately once it had real cores.
batch job (sbatch)
the normal way. write a .sh, submit it, close your laptop, come back later.
run_job.sh:
#!/bin/bash
#SBATCH --account=<your-account>
#SBATCH --partition=a100-40gb
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-task=16
#SBATCH --mem=64G
#SBATCH --time=8:00:00
#SBATCH --job-name=finetune
#SBATCH --output=logs/%x_%j.out
#SBATCH --error=logs/%x_%j.err
module load conda
conda activate <env-name>
cd $SLURM_SUBMIT_DIR
python finetune.py --epochs 100 --patience 15
#SBATCHlines are comments to bash and directives to slurm. they must be at the top, before any real command%x= job name,%j= job id. so logs don’t overwrite each other$SLURM_SUBMIT_DIR= the directory you ransbatchfrom
submit:
sbatch run_job.sh
it prints a job id and returns immediately. that’s the whole point.
chain a few at once:
sbatch job_a.sh && sbatch job_b.sh && sbatch job_c.sh
all three queue up and run whenever gpus free up. i submitted three training variants before bed and read all three logs in the morning 🎉
one-off jobs without writing a file
--wrap takes a command string instead of a script:
sbatch --account=<your-account> --partition=a10 --gres=gpu:1 \
--cpus-per-task=4 --mem=165G --time=4:00:00 \
--wrap="python $(pwd)/extract_landmarks.py --inference-root /path/to/outputs"
use $(pwd) so the path is absolute. the compute node does not start where you are.
checking on jobs
squeue -u $USER # my queue
scancel <jobid> # kill one
scancel -u $USER # kill all mine
ST column is the one to read:
RrunningPDpending — waiting for a free gpu. totally normal, not an errorCGcompleting
my first few “why isn’t it running” panics were just PD. someone else had the gpu. nothing to fix, just wait.
after it finishes:
sacct -j <jobid> --format=JobID,JobName,State,Elapsed,MaxRSS
seff <jobid> # efficiency summary, did i over-request
long jobs that aren’t slurm jobs
moving a couple TB between filesystems isn’t a compute job, but it takes hours and dies the second your ssh drops.
screen keeps the session alive on the login node:
screen # start
# ctrl-a then d -> detach, walk away
screen -rx # reattach
screen -X -S <id> quit # kill a stuck one
related habit i picked up the hard way: put a --resume flag in any script that loops over a big file list. mine died at ~1000/1172 outputs on a full disk, and without --resume that’s the entire run again.
getting data off the cluster
first instinct was rsync:
rsync -avzP <username>@gilbreth.rcac.purdue.edu:/path/on/cluster /local/destination/
-aarchive (keeps permissions, timestamps, recurses)-vverbose-zcompress in transit-Pprogress bar + resume partial files
totally fine for a few GB. for 1.2TB it quoted me 36 hours over my laptop’s connection, so i killed it.
Globus instead:
- install Globus Connect on the machine holding the drive, register it as a collection
- register the external drive as its own collection
- queue the transfer in the Globus web UI
it’s server-to-server, so you can close the laptop and it keeps going. restarts itself on failure. both folders landed over the weekend.
rule i’d give myself: under ~50GB use rsync, above that use Globus.
stuff that cost me the most time
- ran a heavy script on the login node before i understood what a login node was
- forgot
--gres=gpu:1and quietly ran on cpu module loadonly in my shell, not in the job script, so the job couldn’t find python packages- relative paths inside a job script — job starts elsewhere, paths break, job fails 3 seconds in after queueing for an hour
- writing multi-TB outputs to shared
/depotinstead of/scratchand filling the lab’s quota - no
--resume, so a crash at 90% meant starting over
basically every single one is “the compute node is a different computer than the one you typed on.” once that sank in most of it stopped happening.