Job submission
Use the root-level submit.py wrapper for normal runs. It selects the binary
from CMakePresets.json, prepares library paths, creates output directories,
assigns compute and I/O ranks, and requests matching CPU/GPU resources.
Choose a launch mode
| Situation | Use |
|---|---|
| First run or unfamiliar machine | Interactive ./submit.py |
| Repeatable local test | ./submit.py --local ... |
| Production batch run | ./submit.py ... without --local |
| Low-level launcher debugging | Direct MPI commands at the end of this page |
Direct mpirun is intentionally the last option: an incorrect mapping can make
ranks share GPUs, leave I/O ranks without CPU time, or load libraries from the
wrong preset.
If you do not know which inputs to provide, run the wrapper without arguments and answer the prompts step by step:
The wizard detects available CMake presets, checks the configured output engine, and recommends a GPU count so the usual mapping is one MPI task per GPU.
It also shows which fields you need to fill in, explains the important run options, and prints an equivalent command-line invocation at the end. Save that command for future runs when you want to skip the interactive phase.
Command-line mode
Use command-line mode for repeatable local tests and batch submissions.
Local HDF5 test
cd $VVM_ROOT
./submit.py \
--local \
--preset <your_preset_name> \
-c ./rundata/input_configs/default_cases/advection_u.json \
--compute 4
Local run on specific GPUs
For local execution, use VVM_GPU_LIST to select the physical GPU IDs exposed to VVMex ranks. This is the correct way to pin a local run to specific GPUs; --gpus controls the per-node GPU count used by the wrapper, while VVM_GPU_LIST selects the IDs.
cd $VVM_ROOT
VVM_GPU_LIST=0,1,2,3,4,5,6,7 ./submit.py --local \
-c "rundata/input_configs/default_cases/taiwanvvm_2048.json" \
--preset blaze \
--compute 8 \
--nodes 1
SLURM compute run
cd $VVM_ROOT
./submit.py \
--preset <your_preset_name> \
-c ./rundata/input_configs/default_cases/sea_grass_mountain.json \
--compute 16 \
--nodes 1 \
--gpus 16 \
-t 24:00:00
--cpus is omitted on purpose: left alone, the wrapper sizes it to fill the node.
See CPU allocation.
SLURM with SST I/O
cd $VVM_ROOT
./submit.py \
--preset <your_preset_name> \
-c ./rundata/input_configs/default_cases/sea_grass_mountain.json \
--compute 16 \
--io 4 \
--nodes 4 \
--gpus 4 \
--io-cpus 1 \
-t 24:00:00
This is 4 compute ranks and 1 I/O rank per node. --gpus 4 covers the compute
ranks only -- the I/O rank is host-only and needs no device, so the GPU count
follows ceil(compute / nodes) and ignores --io entirely. Omitting --gpus
infers the same number.
Full option reference
Every submit.py flag, with the value used when it is omitted. --config is the
only one that is mandatory in command-line mode; run the wizard instead and it
fills the rest in for you.
Run selection
| Flag | Default | Meaning |
|---|---|---|
-c, --config PATH |
(required) | Case JSON handed to the executable. Its output.engine decides whether I/O ranks are required, and its output.output_dir is created before launch. |
--preset NAME |
(none) | CMakePresets.json entry to take the environment from. It also selects the backend (VVM_ENABLE_GPU) and the binary (binaryDir/vvm), so the launcher can never hand a CPU build a GPU mapping. Without it, library paths are whatever your shell already has. |
--local |
off | Run tools/core_run.sh immediately in this shell instead of submitting it with sbatch. All SLURM-only flags below are ignored. |
Rank and core sizing
| Flag | Default | Meaning |
|---|---|---|
--compute N |
1 |
Simulation MPI ranks. |
--io N |
inferred | Dedicated I/O-server ranks. Inferred as --compute for SST and 0 otherwise. A nonzero value with HDF5 or BP5 is rejected before an allocation is requested. |
--io-cpus N |
1 |
Cores reserved per I/O rank. Does not create I/O ranks. |
--nodes N |
1 |
Nodes to spread the total rank count over. |
--gpus N |
ceil(compute / nodes) |
GPUs per node, covering compute ranks only. Ignored on CPU-only presets, which request no GPUs at all. |
--cpus N |
fills the node, capped per GPU | --cpus-per-task, and the base for OMP_NUM_THREADS. Left unset, the wrapper divides the smaller of the partition's CPUEfctv and the CPUs the requested GPUs entitle the job to by tasks per node; it falls back to 1 where SLURM cannot answer. See CPU allocation. |
--omp-threads N |
--cpus |
OpenMP/Kokkos threads per compute rank, held fixed while --cpus varies. Useful on GPU runs where a rank wants cores for the launch loop but not an OpenMP worker on each. |
SLURM job
Ignored under --local.
| Flag | Default | Meaning |
|---|---|---|
-t, --time HH:MM:SS |
24:00:00 |
Wall-time limit. |
-A, --account NAME |
MST114418 |
Charging account. Override this on any other system. |
-p, --partition NAME |
normal |
Partition. Also the partition the wrapper queries when sizing --cpus. A name SLURM does not know falls back to the cluster's default partition. |
--job-name NAME |
VVMex |
--job-name. |
--out PATH |
<output.output_dir>/%j.out |
Standard output file; %j expands to the job ID. Defaults into the run's output directory. Its directory is created. |
--err PATH |
<output.output_dir>/%j.err |
Standard error file, alongside the standard output file. |
Advanced SLURM
Command-line only — the wizard does not prompt for these.
| Flag | Default | Meaning |
|---|---|---|
--exclusive / --no-exclusive |
--exclusive |
Whether to hold whole nodes. Exclusive is the default because shared nodes make the CPU-affinity split unreliable. |
--export VALUE |
ALL |
sbatch --export. |
--nodelist LIST |
(none) | Restrict to specific nodes. |
--exclude LIST |
(none) | Exclude specific nodes. |
--contiguous |
off | Request contiguous nodes. |
--slurm-arg ARG |
(none) | Append a raw sbatch argument. Repeatable: --slurm-arg='--qos=debug' --slurm-arg='--mem=0'. |
Environment variables you set
| Variable | Applies to | Meaning |
|---|---|---|
VVM_GPU_LIST |
local GPU runs | Comma-separated physical GPU IDs to expose, e.g. 0,1,2,3. Ranks map onto this list in local-rank order. Unset, they map modulo --gpus. CPU presets ignore it. |
VVM_OMP_THREADS |
any run | Same effect as --omp-threads, honoured straight from the caller's environment: VVM_OMP_THREADS=4 ./submit.py .... Reported as [Info] OMP_NUM_THREADS overridden: 11 -> 4. |
VVM_EXTRA_LD_LIBRARY_PATH |
any run | Prepended ahead of the preset's own library directories, so one library can be swapped — an ADIOS2 built without Kokkos, say — without editing CMakePresets.json. Reported as [Info] Honouring caller VVM_EXTRA_LD_LIBRARY_PATH. |
Environment variables the wrapper sets
submit.py exports these for tools/core_run.sh and the executable. They are
listed so the launcher banner and [GPUMap] lines are readable — setting them by
hand does not change the allocation SLURM was asked for, and will desynchronise
the two.
VVM_ROOT, VVM_BACKEND, VVM_BINARY, VVM_CONFIG_FILE, VVM_ARGS,
VVM_COMPUTE_TASKS, VVM_IO_TASKS, VVM_TOTAL_TASKS, VVM_COMPUTE_PER_NODE,
VVM_IO_PER_NODE, VVM_GPUS, VVM_IO_CPUS, VVM_IO_ENGINE, VVM_OUTPUT_DIR,
VVM_ENV_SCRIPT, OMP_NUM_THREADS.
Executable options
submit.py builds this command line for you. It matters only when running
mpirun by hand:
| Argument | Meaning |
|---|---|
path/to/config.json |
First non-flag argument selects the case JSON. |
--io-tasks N |
Reserve the last N ranks as I/O servers. Only meaningful with output.engine = "SST". |
Choosing resources
--compute is the number of simulation MPI ranks. --io is the number of dedicated I/O ranks used when output.engine is SST. The total MPI size is:
For GPU runs, request enough GPUs so compute ranks do not unexpectedly share devices. For local runs, set VVM_GPU_LIST when you need specific physical GPU IDs. A common starting point is:
I/O ranks are host-only. They read and write SST streams and never initialize Kokkos, so they do not consume a GPU and are not counted when the wrapper sizes the GPU request. Two consequences:
- I/O ranks may outnumber the GPUs.
--compute 1 --io 4on a single GPU is a valid configuration. Only compute ranks are mapped onto devices. - I/O ranks get their own core count, set with
--io-cpus(default 1). Under SLURM the wrapper reservesio ranks x --io-cpuscores per node and gives the remainder to the compute ranks, rather than splitting the node evenly across both roles. The launcher banner reports the split:
CPU allocation
--cpus is passed straight through to --cpus-per-task, which makes it the
single knob that decides how much of each node the job actually holds. SLURM
allocates:
Under-request it and the rest of the node is not merely unused, it is
unavailable: the cgroup confines every rank and every thread they spawn to the
CPUs that were asked for. Over-request it on a site that rations cores per GPU
and the job never schedules at all. Note that --exclusive, the default here,
hands the job the whole node regardless, so the request matters most on shared
GPU partitions -- run those with --no-exclusive.
Leave it unset
Omitted, the wrapper fills the node, but never asks for more CPUs than the
requested GPUs entitle the job to. It takes a node name from the partition with
sinfo, reads that node's effective CPU count and GPU count with scontrol,
and divides the smaller of the two budgets by tasks per node:
[Info] CPUs per task: 6 (104 usable CPUs/node / 16 tasks/node)
[Info] CPUs per task: 28 (2 GPUs/node x 28 CPUs/GPU / 2 tasks/node)
The GPU cap matters on sites that ration cores per GPU. Dividing a whole node by
this job's task count over-requests whenever the job uses fewer GPUs than the
node has -- a single-GPU run on a 4-GPU node would ask for four times its share,
and be rejected or left pending rather than scheduled. The per-GPU figure comes
from the partition's DefCpuPerGPU where the site sets one, otherwise from the
node's CPU count divided by its GPU count. A partition-level MaxCPUsPerNode
caps the budget as well. CPU-backend runs request no GPUs, so no cap applies and
they still fill the node.
It reads CPUEfctv rather than CPUTot on purpose. A node can advertise more
CPUs than a job is permitted to hold, and requesting the difference gets the job
rejected rather than scheduled. Where SLURM cannot answer -- --local, no SLURM
at all -- it falls back to 1 and prints the reason. A --partition SLURM does
not know is replaced by the cluster's default partition, since every query above
depends on the partition being real:
Two situations get a warning rather than a silent number, both of which appear
when many I/O ranks share a rationed node with few GPUs. --cpus-per-task is one
value for every rank in the job, so a node's entitlement has to cover compute and
I/O ranks alike:
[Warning] 9 tasks/node x 1 CPU needs 9 CPUs, above the 4 this job is entitled to.
[Warning] about 1 core(s) per compute rank: CPU affinity stays off below 2.
Lower --io, or set --cpus explicitly where the site permits it.
Why --cpus 1 is slow
It is a valid request and it will run, but on a node with many cores it is a severe under-allocation. A concrete case: 8 compute + 8 I/O ranks per node on a 104-CPU node.
--cpus 1 |
--cpus 6 (the default here) |
|
|---|---|---|
| CPUs allocated per node | 16 | 96 |
| Cores per compute rank | 1 | 11 |
| CPU affinity | disabled | NUMA-local |
At one core per rank, that core has to carry the CUDA kernel-launch loop, the MPI halo exchanges, the NCCL host side, and the CUDA driver threads -- while sharing itself with an I/O rank. The GPU goes idle waiting on its host thread, which shows up as low GPU utilization rather than as any obvious error.
CPU affinity
Affinity is automatic and needs no flags. Every rank derives the same partition
independently, from the CPU set the cgroup actually granted
(/proc/self/status) and the PCI/NUMA topology in sysfs:
- I/O ranks take a reserved slice at the end of the CPU list,
--io-cpuswide each. Compute ranks never touch it. - Compute ranks are placed on the NUMA node their GPU is attached to, so the launch loop and its memory sit on the right socket.
OMP_NUM_THREADSis clamped to the cores a rank actually owns, so the OpenMP pool cannot exceed its share.
The [GPUMap] lines report the decision:
[GPUMap] ... local_rank=0 CUDA_VISIBLE_DEVICES=0 cpus=0,1,...,11 bind=numa0
[GPUMap] ... local_rank=8 role=io cpus=88 bind=io_reserved
bind=numa<N> is the intended result. bind=contiguous means the topology was
unreadable and the CPU pool was split in rank order instead. bind=off:<reason>
means affinity declined, and the reason says why:
bind= |
meaning |
|---|---|
numa<N> |
placed on the NUMA node of this rank's GPU |
contiguous |
no usable topology; pool split in local-rank order |
io_reserved |
I/O rank on its reserved slice |
off:too_few_cpus |
fewer than 2 cores per compute rank |
off:no_taskset, off:no_cpuset |
taskset or /proc unavailable |
off:too_few_cpus is deliberate, and it is what --cpus 1 produces. Pinning 16
ranks onto 16 CPUs would stop the kernel lending a blocked I/O rank's core to a
busy compute rank and buy nothing back, so the wrapper leaves the ranks unbound
where binding would hurt.
Separating cores from OpenMP threads
Cores/compute rank also becomes OMP_NUM_THREADS. On a GPU-resident run that
is not always wanted: a compute rank needs cores for the launch loop, the CUDA
driver, NCCL and MPI progress, but not necessarily an OpenMP worker on each one,
and idle Kokkos/OpenMP workers spin at barriers. VVM_OMP_THREADS holds the
thread count fixed while --cpus varies:
VVM_OMP_THREADS=4 ./submit.py --preset <your_preset_name> -c my_config.json \
--compute 64 --io 64 --nodes 8 --gpus 8
The I/O server spends its time in ADIOS2 and HDF5 and is effectively single-threaded, so the default of one core per I/O rank is usually right. Raise it only if the I/O ranks are demonstrably the bottleneck.
This requires ADIOS2 to be built without Kokkos support; see GPU environment. With a Kokkos-enabled ADIOS2, each I/O rank opens a CUDA context anyway and the wrapper's GPU accounting no longer matches reality. VVMex warns about this at configure time.
If you use fewer GPUs than compute tasks per node, the wrapper will warn that MPI ranks may share GPUs.
What the wrapper does
- Sets
VVM_ROOTfrom the script location. - Loads compilers and library paths from the selected
CMakePresets.jsonentry. - Creates the configured output and log directories.
- Writes
submit_command.shand thecode_snapshot/provenance into the output directory. - Requires
--iowhen the JSON usesoutput.engine = "SST". - Rejects nonzero
--iofor HDF5 and BP5 before an allocation is requested. - Runs
tools/core_run.shlocally or submits it throughsbatch. - Maps local MPI ranks to visible GPU IDs and exports runtime variables used by the executable.
- Hides all GPUs from I/O server ranks, and gives them
--io-cpuscores each. - Sizes
--cpusto fill the node when it is not given, capped by the CPUs the requested GPUs entitle the job to. - Falls back to the cluster's default partition when
--partitionnames one SLURM does not have. - Pins each rank to its own cores, compute ranks NUMA-local to their GPU.
- Lets waiting I/O ranks yield the CPU while compute ranks keep spinning, so a blocked I/O rank does not take cycles from the compute ranks it is waiting on.
What the wrapper writes into the output directory
Every run drops its provenance next to the model output, so a finished run can be read back and repeated without guessing what produced it.
| Path | Contents |
|---|---|
%j.out / %j.err |
SLURM logs, unless --out / --err say otherwise. |
submit_command.sh |
Executable script holding the exact submit.py command that started the run, with VVM_ROOT exported and a cd into it. Run it to repeat the run. |
code_snapshot/ |
Copy of the source tree as it was at submit time, plus the case JSON and any profile/spatial input files. |
code_snapshot/ORIGINAL_GIT_VERSION.txt |
Commit, branch, git describe, upstream, remote, author, date, subject, and the git status --porcelain listing of the source repository at submit time. |
code_snapshot/ORIGINAL_GIT_UNCOMMITTED.patch |
The uncommitted diff at submit time. Written only when the source tree was dirty. |
The snapshot is itself a git repository. Its single commit reproduces the source
repository's HEAD exactly, and any uncommitted local edits are left in the working
tree, so git -C <output_dir>/code_snapshot diff shows precisely what the run
carried on top of that commit.
Direct MPI commands
Direct MPI remains useful for small debugging sessions after the environment has already been prepared.
Advanced only: These commands bypass the wrapper's allocation checks. On multi-node or multi-GPU runs, make sure rank placement, GPU visibility, CPU binding, OpenMP threads, and I/O ranks are correct before trusting performance numbers.
For asynchronous I/O, reserve the final ranks for I/O servers: