Apps#
Launching MPI applications across a job’s allocation with knit run.
Register an MPI app#
Declare an app with knit_register_app; its body runs once per MPI rank, and knit records outputs from rank 0 only.
APIs: knit_register_app, knit_output
An app is the unit knit run launches across an MPI world: knit starts one
copy of its body per rank. Register it with knit_register_app — it takes
parameters, types, and outputs exactly like a job or a plain command:
@app "render" "Render one MPI-parallel Julia-set image."
@with_optional "width:integer" "800" "Image width in pixels."
@with_optional "height:integer" "600" "Image height in pixels."
@with_optional "c-re:real" "-0.8" "Real part of the Julia constant c."
@with_optional "c-im:real" "0.156" "Imaginary part of the Julia constant c."
@with_optional "max-iter:integer" "1000" "Maximum iterations per pixel."
@with_optional "colormap:string" "fire" "Palette: grayscale | fire | ocean."
@with_required "output:string" "Absolute PNG path (the job supplies one per run)."
@with_output "inside:integer" "0" "Grid points inside the set (recorded by rank 0)."
_render_app() {
local width height c_re c_im max_iter colormap output
width=$(knit_get_parameter "width" "$@")
height=$(knit_get_parameter "height" "$@")
c_re=$(knit_get_parameter "c-re" "$@")
c_im=$(knit_get_parameter "c-im" "$@")
max_iter=$(knit_get_parameter "max-iter" "$@")
colormap=$(knit_get_parameter "colormap" "$@")
output=$(knit_get_parameter "output" "$@")
# This body runs on EVERY rank: knit run launched one copy per rank, and
# julia-fractal --- called as a child, not exec'd --- inherits the launcher's
# MPI environment, so the copies form one MPI world and split the image. Only
# rank 0 writes the PNG and prints the inside= line.
local out
out=$(julia-fractal "${width}" "${height}" "${c_re}" "${c_im}" "${max_iter}" \
"${output}" "${colormap}")
printf '%s\n' "${out}"
# Record the metric. Knit records outputs only from rank 0 (it suppresses
# recording on the other ranks), so this single knit_output writes one row no
# matter how many ranks ran. KNIT_MPI_RANK / KNIT_MPI_SIZE / KNIT_MPI_LOCAL_RANK
# are available too if a body needs to branch on its own rank.
local inside
inside=$(sed -n 's/.*inside=\([0-9]*\).*/\1/p' <<< "${out}" | head -1)
knit_output "inside" "${inside}"
}
@done
knit_register_app "render" _render_app "Render one MPI-parallel Julia-set image."
knit_with_optional "width:integer" "800" "Image width in pixels."
knit_with_optional "height:integer" "600" "Image height in pixels."
knit_with_optional "c-re:real" "-0.8" "Real part of the Julia constant c."
knit_with_optional "c-im:real" "0.156" "Imaginary part of the Julia constant c."
knit_with_optional "max-iter:integer" "1000" "Maximum iterations per pixel."
knit_with_optional "colormap:string" "fire" "Palette: grayscale | fire | ocean."
knit_with_required "output:string" "Absolute PNG path (the job supplies one per run)."
knit_with_output "inside:integer" "0" "Grid points inside the set (recorded by rank 0)."
_render_app() {
local width height c_re c_im max_iter colormap output
width=$(knit_get_parameter "width" "$@")
height=$(knit_get_parameter "height" "$@")
c_re=$(knit_get_parameter "c-re" "$@")
c_im=$(knit_get_parameter "c-im" "$@")
max_iter=$(knit_get_parameter "max-iter" "$@")
colormap=$(knit_get_parameter "colormap" "$@")
output=$(knit_get_parameter "output" "$@")
# This body runs on EVERY rank: knit run launched one copy per rank, and
# julia-fractal --- called as a child, not exec'd --- inherits the launcher's
# MPI environment, so the copies form one MPI world and split the image. Only
# rank 0 writes the PNG and prints the inside= line.
local out
out=$(julia-fractal "${width}" "${height}" "${c_re}" "${c_im}" "${max_iter}" \
"${output}" "${colormap}")
printf '%s\n' "${out}"
# Record the metric. Knit records outputs only from rank 0 (it suppresses
# recording on the other ranks), so this single knit_output writes one row no
# matter how many ranks ran. KNIT_MPI_RANK / KNIT_MPI_SIZE / KNIT_MPI_LOCAL_RANK
# are available too if a body needs to branch on its own rank.
local inside
inside=$(sed -n 's/.*inside=\([0-9]*\).*/\1/p' <<< "${out}" | head -1)
knit_output "inside" "${inside}"
}
knit_done
The body above runs on every rank. It calls julia-fractal as a child
process (not exec), so that child inherits the launcher’s MPI environment and
the copies join one MPI_COMM_WORLD and split the work. A body that needs to
branch on its own rank reads the environment knit normalizes for it — see
Read a rank’s place in the MPI world.
Recording is automatic but rank-0 only: knit suppresses row and output
recording on every rank but rank 0, so a single knit_output (see the
inside metric above) writes one row no matter how many ranks ran. You do not
need to guard it with a rank check.
An app is not submitted directly — a job launches it with knit run (see
Launch an app from a job).
Read a rank’s place in the MPI world#
An app body branches on its own rank via the normalized KNIT_MPI_RANK, KNIT_MPI_SIZE, and KNIT_MPI_LOCAL_RANK.
APIs: KNIT_MPI_RANK, KNIT_MPI_SIZE, KNIT_MPI_LOCAL_RANK
An app body runs once per rank. When it needs to know which rank it is — to split work, or to let one rank do something the others don’t — it reads the three environment variables knit exports into every rank:
KNIT_MPI_RANK— this rank’s index inMPI_COMM_WORLD(0-based).KNIT_MPI_SIZE— the total number of ranks inMPI_COMM_WORLD.KNIT_MPI_LOCAL_RANK— this rank’s index among the ranks on its own node.
if [[ "${KNIT_MPI_RANK}" == "0" ]]; then
printf 'world has %s ranks\n' "${KNIT_MPI_SIZE}"
fi
Knit fills these from whichever launcher actually ran — Open MPI
(OMPI_COMM_WORLD_*), MPICH/Hydra (PMI_*), Slurm (SLURM_PROCID /
SLURM_NTASKS / SLURM_LOCALID), PALS (PALS_*), and so on. Reading the
normalized KNIT_MPI_* names means the same app body works unchanged across
backends, instead of hard-coding one launcher’s variables.
On a laptop (the none launcher, a single process) KNIT_MPI_RANK is 0,
KNIT_MPI_SIZE is 1, and KNIT_MPI_LOCAL_RANK is 0, so a body
written against these variables still runs correctly with no launcher at all.
You rarely need to guard knit_output with a rank check: knit already records
only from rank 0 (see Register an MPI app). These variables are for the app’s
own logic — deciding which slice of the problem each rank computes.
Launch an app from a job#
Call knit run –procs N – <app> from a job body to launch an app across the job’s allocation.
APIs: run, knit_register_app, knit_job_nodecount
Apps are launched from inside a job body with knit run. The job holds the
scheduler allocation; knit run places the app’s ranks across it:
@job "julia" "Render a Julia-set fractal as a submitted job."
@with_setup "juliaenv"
@with_optional "width:integer" "800" "Image width in pixels."
@with_optional "height:integer" "600" "Image height in pixels."
@with_optional "c-re:real" "-0.8" "Real part of the Julia constant c."
@with_optional "c-im:real" "0.156" "Imaginary part of the Julia constant c."
@with_optional "max-iter:integer" "1000" "Maximum iterations per pixel."
@with_optional "colormap:string" "fire" "Palette: grayscale | fire | ocean."
_julia() {
local width height c_re c_im max_iter colormap output
width=$(knit_get_parameter "width" "$@")
height=$(knit_get_parameter "height" "$@")
c_re=$(knit_get_parameter "c-re" "$@")
c_im=$(knit_get_parameter "c-im" "$@")
max_iter=$(knit_get_parameter "max-iter" "$@")
colormap=$(knit_get_parameter "colormap" "$@")
local png="${KNIT_JOB_PREFIX%/}/fractal.png"
# Report where the scheduler placed this job.
printf 'The job is running on hosts: %s\n' \
"$(knit_job_hostnames --separator ', ')"
# Was (Step 4): julia-fractal "${width}" ... "${png}" "${colormap}"
# Now: launch the render app instead of running the binary here, with one rank
# per allocated node. knit_job_nodecount is 1 on a laptop, so this runs
# anywhere; on a cluster it scales with the nodes the job was given (submit
# --nodes N). The values we used to pass positionally become named parameters.
knit run --procs "$(knit_job_nodecount)" --procs-per-node 1 -- render \
--width "${width}" --height "${height}" \
--c-re "${c_re}" --c-im "${c_im}" --max-iter "${max_iter}" \
--colormap "${colormap}" --output "${png}"
}
@done
knit_register_job "julia" _julia "Render a Julia-set fractal as a submitted job."
knit_with_setup "juliaenv"
knit_with_optional "width:integer" "800" "Image width in pixels."
knit_with_optional "height:integer" "600" "Image height in pixels."
knit_with_optional "c-re:real" "-0.8" "Real part of the Julia constant c."
knit_with_optional "c-im:real" "0.156" "Imaginary part of the Julia constant c."
knit_with_optional "max-iter:integer" "1000" "Maximum iterations per pixel."
knit_with_optional "colormap:string" "fire" "Palette: grayscale | fire | ocean."
_julia() {
local width height c_re c_im max_iter colormap output
width=$(knit_get_parameter "width" "$@")
height=$(knit_get_parameter "height" "$@")
c_re=$(knit_get_parameter "c-re" "$@")
c_im=$(knit_get_parameter "c-im" "$@")
max_iter=$(knit_get_parameter "max-iter" "$@")
colormap=$(knit_get_parameter "colormap" "$@")
local png="${KNIT_JOB_PREFIX%/}/fractal.png"
# Report where the scheduler placed this job.
printf 'The job is running on hosts: %s\n' \
"$(knit_job_hostnames --separator ', ')"
# Was (Step 4): julia-fractal "${width}" ... "${png}" "${colormap}"
# Now: launch the render app instead of running the binary here, with one rank
# per allocated node. knit_job_nodecount is 1 on a laptop, so this runs
# anywhere; on a cluster it scales with the nodes the job was given (submit
# --nodes N). The values we used to pass positionally become named parameters.
knit run --procs "$(knit_job_nodecount)" --procs-per-node 1 -- render \
--width "${width}" --height "${height}" \
--c-re "${c_re}" --c-im "${c_im}" --max-iter "${max_iter}" \
--colormap "${colormap}" --output "${png}"
}
knit_done
The launch line is the last statement of the job:
knit run --procs "$(knit_job_nodecount)" --procs-per-node 1 -- render \
--width "${width}" --output "${png}"
Everything before the -- configures the launch (how many ranks, where);
everything after names the app and its own parameters. Deriving --procs from
knit_job_nodecount makes the same body scale: it is 1 on a laptop (one
local rank, no MPI needed) and grows to the node count on a cluster, set by
submit --nodes N.
knit run records the launch as a row in the runs table — the resolved
placement and the app name travel with it — and it must run inside a job
(it reads the surrounding job’s allocation). See Control process placement to
pin ranks precisely and Pick a launcher backend to choose the launcher.
Control process placement#
Shape how knit run places ranks with –procs, –procs-per-node, –hostnames, and per-rank CPU/GPU binding options.
APIs: run
knit run takes a set of placement options, all before the --. Give as few
or as many as you like: knit fills the rest from the job’s allocation and the
machine’s per-node core count.
$ knit run --procs 32 --procs-per-node 8 -- render --output "${png}"
How many ranks and where:
--procs— total number of ranks. If omitted, knit uses one rank per allocated core (per-node core count × nodes), or one rank per node when the core count is unknown.--procs-per-node— ranks per node. If omitted, the launcher’s default distribution applies;--procsand--procs-per-nodemust be consistent with the node count (--procsdivisible by--procs-per-node).--hostnames— a comma-separated subset of the job’s allocated hosts to place on. Each must be one of the job’s hosts (seeknit_job_hostnames); useful to run on part of a larger allocation.
Per-rank CPU and GPU binding (all optional, best-effort — each is translated to the chosen launcher’s native flag, and a backend with no equivalent warns and skips rather than failing):
--cpus-per-proc— hardware threads reserved per rank.--bind— CPU binding policy. Knit normalizes the vocabularynone | core | socket | numa | threadto each launcher’s spelling; any other value is passed through verbatim.--gpus-per-proc— GPUs reserved per rank.--gpu-bind— GPU binding policy, passed through to the launcher.
The resolved placement is recorded with the run, so a run’s records show exactly how its ranks were laid out. For anything a flag doesn’t cover, pass launcher arguments verbatim (see Pick a launcher backend).
Pick a launcher backend#
Override the launcher knit run uses with –launcher, and pass launcher-native flags with –launcher-args.
APIs: run, bootstrap
knit run selects a launcher automatically — the one detected at bootstrap
or set with bootstrap --launcher (see Select the MPI launcher). Override it
for a single launch with --launcher:
$ knit run --launcher openmpi --procs 16 -- render --output "${png}"
The supported backends are:
none— no launcher; runs the app as a single rank-0 process (rejects--procs> 1 and remote hosts). This is the laptop default.openmpi/mpich— MPI-native launchers (mpirun/mpiexec).slurm/pbs/pals/flux— the scheduler’s own launcher (srun, PBSmpiexec, PALSmpiexec,flux run), which reads the allocation natively.
Knit translates --procs, --procs-per-node, --hostnames, and the
binding options into each backend’s native flags. When a flag isn’t covered, or
you need a launcher option knit doesn’t model, pass it through verbatim with
--launcher-args:
$ knit run --launcher openmpi --procs 16 \
--launcher-args "--map-by socket --report-bindings" -- render --output "${png}"
--launcher-args is the escape hatch: its contents are appended to the
launcher command line unchanged, after the flags knit generates.