Close your laptop, lose your wifi, quit VS Code — come back tomorrow and your session is exactly where you left it.
The normal way to get a shell on a compute node:
srun --pty bash # or the "interactive" button in Open OnDemand
ssubmit -i -t 1d -m 32g
This works right up until any of the following happens:
Your shell is a child of the SSH connection. Kill the connection, kill the shell, kill everything running in it. Two hours of an assembly, gone.
Three moving parts, and you only ever type two commands.
Terminal or VS Code
Disposable. Can disappear at any time.
You SSH here
Also disposable — you just pass through it.
A batch job whose only task is to run a tmux server, forever
Slurm does not care whether you are connected.
tmux is a program that runs on the server and holds your shells for you. Your shell becomes a child of tmux instead of a child of your SSH connection — so when the connection dies, the shell doesn't notice.
It also gives you multiple windows and split panes over a single connection, which is a nice bonus.
tmux new -s work # start a new session called "work"
tmux ls # list sessions that exist
tmux attach -t work # reattach to it
# Ctrl-b then d = detach and leave it running
Detaching is not quitting. Ctrl-b d walks away and leaves everything running. Typing exit actually closes the shell.
Every tmux command is: press Ctrl-b, let go, then press one more key.
echo 'set -g mouse on' >> ~/.tmux.conf
Now you can click between panes, drag the borders to resize, and scroll with the wheel.
Paste into ~/.bashrc, ~/.zshrc, ~/.aliases — wherever you keep this sort of thing — then source that file. Everything in this talk runs in both bash and zsh.
TMUX_JOB_NAME="tmuxjob" # "interactive" collides with other people's jobs
TMUX_JOB_CPUS=8
# NOT $SHELL - that is your /etc/passwd shell, which is bash on Bunya
# even if zsh is what you actually use
if [ -n "$ZSH_VERSION" ]; then TMUX_JOB_SHELL="zsh"; else TMUX_JOB_SHELL="bash"; fi
start_tmux_job() { # submit the job
echo "[INFO] Submitting interactive tmux job..."
# SSUBMIT_* are only defaults - passing -m/-t to the function still wins
SSUBMIT_MEMORY=32g SSUBMIT_TIME=1d \
ssubmit "$@" "$TMUX_JOB_NAME" \
"tmux new-session -d -s worker $TMUX_JOB_SHELL -l; while tmux has-session -t worker 2>/dev/null; do sleep 30; done" \
-- -c "$TMUX_JOB_CPUS"
echo "[INFO] Job submitted. Wait a moment, then type 'join_tmux_job'."
}
join_tmux_job() { # attach to it
local target_job
target_job=$(squeue -u "$USER" -n "$TMUX_JOB_NAME" -h -t RUNNING -o "%i" | head -n 1)
if [ -z "$target_job" ]; then
echo "[ERROR] No running '$TMUX_JOB_NAME' job found." >&2
return 1
fi
echo "[INFO] Attaching to tmux on worker node (Job ID: $target_job)..."
srun --jobid="$target_job" --overlap --pty tmux attach-session -t worker
}
start_tmux_job -m 32g -t 3d # once. 32 GB of RAM, 3 day walltime
squeue # wait until State is RUNNING (a few seconds, usually)
join_tmux_job # you are now on a compute node, inside tmux
# ... do your work: run things, edit things, leave things running ...
# Ctrl-b then d -> detach
# close the laptop, go home, get on a plane
join_tmux_job # tomorrow: everything exactly as you left it
tmux kill-session -t worker # finished for good - this also ends the Slurm job
That's it. start_tmux_job once, then join_tmux_job every time you sit down.
Killing the tmux session releases the node within about 30 seconds — you don't have to remember a scancel. scancel still works if you'd rather.
SSUBMIT_MEMORY=32g SSUBMIT_TIME=1d ssubmit "$@" "$TMUX_JOB_NAME" \
"tmux new-session -d -s worker $TMUX_JOB_SHELL -l; while tmux has-session -t worker 2>/dev/null; do sleep 30; done" \
-- -c "$TMUX_JOB_CPUS"
start_tmux_job -m 64g -t 3d still overrides them.-d means "detached": create the session but attach nothing, which is the only thing that makes sense in a batch job. The shell is named explicitly because tmux otherwise takes it from $SHELL.-- goes straight to sbatch.squeue -u "$USER" -n "$TMUX_JOB_NAME" -h -t RUNNING -o "%i" | head -n 1
Find the job ID: my jobs (-u), named tmuxjob (-n), currently RUNNING (-t), no header (-h), print only the ID (-o "%i").
srun --jobid="$target_job" --overlap --pty tmux attach-session -t worker
--overlap says "share them".Everything you run inside that tmux uses the job's CPUs and memory, not the login node's. So this isn't just about persistence — it's also the correct way to run anything heavy interactively.
join_tmux_job. That's the whole integration./home and /scratch are the same filesystem from the compute node.Gotcha: Ctrl-b is a VS Code shortcut (toggle sidebar), so VS Code eats it before tmux sees it. Two fixes — pick one.
Let the terminal have it — VS Code settings.json:
"terminal.integrated.commandsToSkipShell": [ "workbench.action.toggleSidebarVisibility" ]
Or move the tmux prefix — ~/.tmux.conf on Bunya:
unbind C-b set -g prefix C-a bind C-a send-prefix
-t 3d is up, Slurm kills the job and tmux goes with it. Nothing you left running survives.tmuxjob at a time. Two of them and join_tmux_job silently takes the first one it finds.tmuxjob.out and tmuxjob.err into your current directory — check tmuxjob.err if the job dies immediately.TMUX_JOB_SHELL is decided when you source the file, so if you submit from bash and later attach from zsh, the session is still bash until you resubmit.exit closes. If you exit out of every window, the tmux session is gone and you'll need to resubmit.The srun --overlap trick isn't specific to tmux. You can drop a shell into any job of yours that's currently running:
srun --jobid=12345678 --overlap --pty bash # any job ID from squeue
Now htop, nvidia-smi, tail a log — while it's still going. Useful for "is this actually using the 200 GB I asked for?"
joinjob, or joinjob 12345678joinjob() {
local target_job="$1"
# No arrays: zsh indexes them from 1 and bash from 0, so ${arr[0]}
# silently expands to an empty job ID under zsh.
if [ -z "$target_job" ]; then
target_job=$(squeue -u "$USER" -n "$TMUX_JOB_NAME" -h -t RUNNING -o "%i" | head -n 1)
fi
if [ -z "$target_job" ]; then
echo "[ERROR] No running job named '$TMUX_JOB_NAME' found for user $USER." >&2
return 1
fi
echo "[INFO] Attaching to job: $target_job"
srun --jobid="$target_job" --overlap --pty "$TMUX_JOB_SHELL" -l
}
With no argument it joins your tmux job; give it a job ID and it joins anything. Runs $TMUX_JOB_SHELL, so you get whichever shell you actually use.
No aliases needed. Slurm reads these environment variables directly — put them in your ~/.profile or ~/.bashrc.
export SQUEUE_FORMAT="%.10i %.30j %.10T %.10P %.20R %.10q %.17S %.10l %.10L %.6m" export SQUEUE_USERS="$USER" export SACCT_FORMAT="JobID,JobName%30,ExitCode,State,ReqMem,MaxRSS,Timelimit,Elapsed,Start,End" export SLURM_TIME_FORMAT="%X %d/%m/%y"
squeue now shows only your jobs, with 30 characters of job name instead of 8, plus the estimated start time and time remainingsacct now shows ReqMem next to MaxRSS — how much you asked for versus what you actually used. This is how you stop requesting 500 GB for a job that peaks at 12 GB17/08/26 instead of ISO timestampsBonus alias: alias sq='squeue'. You type it fifty times a day.
On your laptop, in ~/.ssh/config:
Host *
ServerAliveInterval 10
ServerAliveCountMax 5
Host bunya bunya.ssh
User uqXXXXXX
HostName bunya.rcc.uq.edu.au
ControlMaster auto
ControlPersist 10m
ControlPath ~/.ssh/control-%r@%h:%p
ssh bunyaIf you force a login shell with RemoteCommand zsh -l and RequestTTY force (handy — it makes ssh bunya give you your real shell), keep a second alias without those lines. scp, rsync and VS Code Remote-SSH all break on a forced RemoteCommand. That's what the bunya.ssh name above is for.
umask 002 # everything you create is group-readable and group-writable
Put it in your ~/.profile. Saves an enormous amount of "can you chmod that for me" over a year.
export BAKTA_DB=/scratch/project_mnt/S0256/bakta/db/v6.0_20250224/db export CHECKM2DB=/scratch/opendata/genomics/CheckM2/version_3/CheckM2_database/uniref100.KO.1.dmnd export MOB_SUITE_DB=/scratch/project_mnt/S0256/mob-suite/db export ATB_DATA_DIR=/scratch/project_mnt/S0256/databases/atb-cli
Everything from this talk, in one block. Drop it in whichever shell config file you use and source it. Tested in both bash and zsh.
alias sq='squeue'
export SQUEUE_FORMAT="%.10i %.30j %.10T %.10P %.20R %.10q %.17S %.10l %.10L %.6m"
export SQUEUE_USERS="$USER"
export SACCT_FORMAT="JobID,JobName%30,ExitCode,State,ReqMem,MaxRSS,Timelimit,Elapsed,Start,End"
export SLURM_TIME_FORMAT="%X %d/%m/%y"
umask 002
TMUX_JOB_NAME="tmuxjob"
TMUX_JOB_CPUS=8
if [ -n "$ZSH_VERSION" ]; then TMUX_JOB_SHELL="zsh"; else TMUX_JOB_SHELL="bash"; fi
start_tmux_job() {
echo "[INFO] Submitting interactive tmux job..."
SSUBMIT_MEMORY=32g SSUBMIT_TIME=1d \
ssubmit "$@" "$TMUX_JOB_NAME" \
"tmux new-session -d -s worker $TMUX_JOB_SHELL -l; while tmux has-session -t worker 2>/dev/null; do sleep 30; done" \
-- -c "$TMUX_JOB_CPUS"
echo "[INFO] Job submitted. Wait a moment, then type 'join_tmux_job'."
}
join_tmux_job() {
local target_job
target_job=$(squeue -u "$USER" -n "$TMUX_JOB_NAME" -h -t RUNNING -o "%i" | head -n 1)
if [ -z "$target_job" ]; then
echo "[ERROR] No running '$TMUX_JOB_NAME' job found." >&2
return 1
fi
echo "[INFO] Attaching to tmux on worker node (Job ID: $target_job)..."
srun --jobid="$target_job" --overlap --pty tmux attach-session -t worker
}
joinjob() {
local target_job="$1"
if [ -z "$target_job" ]; then
target_job=$(squeue -u "$USER" -n "$TMUX_JOB_NAME" -h -t RUNNING -o "%i" | head -n 1)
fi
if [ -z "$target_job" ]; then
echo "[ERROR] No running job named '$TMUX_JOB_NAME' found for user $USER." >&2
return 1
fi
echo "[INFO] Attaching to job: $target_job"
srun --jobid="$target_job" --overlap --pty "$TMUX_JOB_SHELL" -l
}
Then: start_tmux_job -m 32g -t 3d → join_tmux_job → Ctrl-b d. Hit Reference (bottom right) for one scrollable page. Dotfiles: github.com/mbhall88/.dotfiles · ssubmit