High-throughput on HPC systems¶
This is the shortest path from an installed httk on the cluster to 1000
SLURM jobs. The example assumes you are logged in to arrhenius, that
my_executable is available there, and that its command-line form is
srun my_executable <int>. Replace <account> and <partition> with values
for your allocation.
The workspace runs one 24-hour httk manager per sbatch allocation. Each
manager has 10 CPUs and runs its workspace jobs one after another inside that
allocation; --count N starts N allocations in parallel.
Set up the workspace and launcher¶
Run this once on the cluster. No httk project is needed: the workspace is
registered under your user, and --global stores the launcher under your user
as well (~/.config/httk/launchers/), so it serves every workspace on this
cluster. The repeatable --setting KEY=VALUE options seed application
settings while the workspace is created.
mkdir ~/my_bulk_runs && cd ~/my_bulk_runs
httk workflow launcher add --template slurm --global arrhenius \
--set slurm.account=<account> --set slurm.partition=<partition> \
--set slurm.time_limit=24:00:00 --set slurm.nodes=1 \
--set slurm.ntasks=10 --set slurm.cpus_per_task=1
httk workspace init --name my_bulk_runs \
--setting manager.launch=arrhenius --setting manager.workers=1 \
--setting environment.prelude="module load my_stack" . # prelude only if needed
Create the jobs¶
Run this from the command line where you normally launch the program. The
{n} parameter is filled for each job; httk generates and stores the one-step
workflow wrapper once:
for n in $(seq 1 1000); do
httk job new --from-command 'srun --ntasks=10 --cpus-per-task=1 my_executable {n}' --parameter n=$n --tag n$n
done
Jobs from a directory of input files¶
When each run needs its own input file rather than an integer, stage the file
into the job with --file and refer to it in the template by the same name.
for f in input_files/*; do
httk job new --from-command 'srun --ntasks=10 --cpus-per-task=1 my_executable {input}' \
--file input="$f" --tag "$(basename "$f" .dat)"
done
--file input=PATH copies the file into the job’s payload (files/input), so
the job carries its own immutable copy, providing provenance for the run even
if input_files/ changes later. At run time, {input} is filled with the
absolute path of that staged copy (a placeholder resolves from --parameter
values or --file names; a name given to both is an error). The program still
runs with the job’s run/ directory as its working directory, so relative
outputs land there as before — and every --file input is also copied into
that directory under its own name before the command starts (never overwriting
a file that is already there), so a program that reads its input from the
working directory, such as VASP reading POSCAR, needs no placeholder at all:
--from-command 'srun … vasp_std' --file POSCAR=POSCAR. --file is repeatable for several inputs, as in
--file geometry=… --file settings=…, and the wrapper it generates is the
same one shown under “Writing the workflow yourself”, with the path
substituted.
Jobs from a directory of input directories¶
A real VASP run needs several input files together — INCAR, KPOINTS,
POSCAR, POTCAR. Keep one directory per run and stage all of its files at
once with --files:
for d in inputs/*/; do
httk job new --from-command 'srun --ntasks=10 --cpus-per-task=1 vasp_std' \
--files "$d" --tag "$(basename "$d")"
done
--files DIR stages every regular file directly inside DIR under its own
name, exactly as one --file NAME=DIR/NAME per file would: the copies live in
the job’s files/ directory, are placed in the working directory before
vasp_std starts, and can be referenced as {INCAR}-style placeholders when a
program takes paths instead. Subdirectories are skipped (with a warning), and a
name that occurs twice — across two --files directories, or in both --files
and --file — is an error, so no input can be silently replaced.
Run and watch the batch¶
Submit 20 manager allocations and return immediately with their SLURM job IDs:
httk workflow run --count 20 # submits 20 allocations; returns at once with their SLURM job ids
squeue -u $USER # the httk-manager jobs
httk job list # ready / running / succeeded per job
httk job why jobs/n17--* # a stuck or failed job
Here --count is the number of managers to start: 20 SLURM allocations
of 10 CPUs each, each running the workspace’s jobs one after another.
--workers is how many jobs ONE manager runs concurrently inside its
allocation; it is 1 here because each my_executable uses all 10 CPUs.
--count/manager.count and --workers/manager.workers can also be set
once on the workspace; command-line options override those settings for one
run.
Outputs are stored in jobs/n<N>--<uuid>/run/, including anything that
my_executable writes there. Its console output is in
jobs/n<N>--<uuid>/logs/stdio.out, with attempt markers.
If a job is interrupted by the 24-hour limit, it returns to the queue and a
later manager retries it. Run httk workflow run --count 20 again until
httk job list shows every job as succeeded. For individual records, use
httk job show and httk job log.
More advanced steps¶
Collecting results into a database¶
httk workflow collect needs the workflow to declare what its outputs are. A
--from-command job has no collector, so collecting it directly would produce
only generic records. To give answer.txt a named output, turn the one-command
job into a small workflow package:
my_executable/
├── httk_workflow.toml
├── run.sh # the same wrapper shown below
└── collect.py
Declare the workflow, runner, output, and collector in
my_executable/httk_workflow.toml:
[workflow]
id = "my_executable"
description = "Run my_executable for one integer parameter."
[workflow.runner]
entry = "run.sh"
steps = ["run"]
initial_step = "run"
data_mode = "none"
workdir_mode = "persistent"
[workflow.outputs.answer]
entry_type = "strings"
ref = "https://example.org/types/answer"
description = "The answer written by my_executable."
[workflow.collect]
file = "collect.py"
The executable collector runs from the package directory. It reads the
handshake line on stdin, then one {"record": ...} line per job. The complete
JobRecord mapping names the job’s run directory as the workspace-relative
workdir_path; combine it with the record’s absolute workspace path so the
hook can read answer.txt there and return one response line for each input
record:
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
handshake = json.loads(next(sys.stdin))
if handshake != {"format": "httk-workflow-collect-stream", "format_version": 2}:
raise ValueError("unexpected collect stream")
for line in sys.stdin:
record = json.loads(line)["record"]
try:
workdir = Path(record["workspace"]) / record["workdir_path"]
value = (workdir / "answer.txt").read_text().strip()
response = {"job_id": record["job_id"], "outputs": {"answer": {"value": value}}}
except OSError as error:
response = {"job_id": record["job_id"], "error": str(error)}
print(json.dumps(response), flush=True)
Make collect.py executable. The package’s run.sh is the same runner shown
in the next subsection. Create jobs from the package in the loop, then collect
them after the runs finish:
for n in $(seq 1 1000); do
httk job new --workflow-dir ./my_executable --parameter n=$n --tag n$n
done
httk workflow collect --into results.sqlite
See the database walkthrough and the httk-workflow collecting documentation.
Writing the workflow yourself¶
The --from-command shortcut generates this run.sh, which can also be used
as the package runner above:
#!/usr/bin/env bash
set -euo pipefail
source "$HTTK_WORKFLOW_BASH_API"
httk_workflow_runner command run
step_run() {
httk_workflow_run -- srun --ntasks=10 --cpus-per-task=1 my_executable "$(httk_workflow_parameter n)"
httk_workflow_succeed
}
httk_workflow_main
Use it directly for a job, for example:
httk job new --from-runner ./run.sh --parameter n=17
Full workflows can declare inputs, resources per step, spawn child jobs, publish data transactionally, and be packaged and versioned. See the full workflow authoring guide, workflow packages, Bash SDK, launchers, and remotes documentation.
Faster job creation¶
For very large sets, use the Python new_jobs(...) streaming form; see the
bulk-runs walkthrough.