Parallelism and the workflow layer

Every stage of an HTESP campaign is a loop over materials: read the tracking file, take entries start through end (exclusive), and do the same thing to each. That loop lives in htesp/workflow.py, in the class HTESPWorkflow, and it runs in parallel.

HTESPWorkflow

HTESPWorkflow is one method per stage. Each method keeps the name and the argument contract of the scan script it replaces – start, end (exclusive), the tracking file, and an optional extra argument – so nothing about how you drive a campaign has changed:

from htesp.workflow import HTESPWorkflow

wf = HTESPWorkflow()                       # reads ./input.in
wf.relax_scan(1, 5, "mpid.in")
wf.create_inputs(1, 5, "mpid.in", nkpt=50)

or from the command line:

python -m htesp.workflow relax-scan 1 5 mpid.in
python -m htesp.workflow create-inputs 1 5 mpid.in 50 --workers 8

You do not normally call it directly. mainprogram dispatches to it, and the shims in bin/ do too.

bin/ and legacy/bash/

bin/ holds 52 one-line shims, one per former bash scan script, with the same names and the same argument contract. Putting bin/ on $PATH:

export PATH="path_to_HTESP/bin:$PATH"

reproduces exactly what export PATH=$PATH:path_to_HTESP/src/bash used to do. relax-scan 1 5 mpid.in still works and still means the same thing; bin/jobscript.sh is still sourceable, and now records the job ids it submits.

The original bash scripts are kept unmodified under legacy/bash/. They are there to be read – to check what the old behaviour was – not to be run, and they are not installed and not on $PATH.

–workers

The per-material body runs in a process pool:

mainprogram 1 --workers 16
export HTESP_WORKERS=4      # the default for every later command

The default pool size is min(cpu_count, 8). --workers 1 runs the loop serially, which is what you want when you are debugging a single material or reading interleaved output. A pool is not created at all when there is only one material to process.

This is worth having on the login node too, where the work per material is building input files rather than running DFT: a 300-compound mainprogram 4 is bounded by pymatgen and file writing, and a pool cuts it by close to the number of workers.

Per-material scratch directories

The old bash layer wrote fixed-name scratch files into the project root: mass.dat, qpoint.dat, kpoint.dat, BZ.pdf, scf_dir/kpathlines.dat, temp*.in. With one material at a time that was merely untidy. It is why the loop could not simply be backgrounded.

Each material body now runs inside its own private scratch directory, carrying a local scf_dir. The helper modules keep their existing “write next to me” behaviour, and the artefacts they produce are moved to their final destination when the body finishes. Two materials processed at the same time cannot see each other’s temporary files.

The working directory is also restored after every iteration, and a failed cd raises instead of letting the rest of the body – rm -r ..., sbatch ... – run in the project root.

Deterministic output

Results are collected and re-ordered by material index before anything is written. result.csv, econv.csv, elastic.csv and the mpid-*.in lists therefore come out in tracking-file order, whatever order the processes happened to finish in. Two runs over the same inputs produce byte-identical files, so they can be diffed and committed.

–dry-run

mainprogram 4 --dry-run
mainprogram 20 --dry-run

--dry-run builds every input file and does all the file plumbing, but never calls the scheduler. Use it to check a campaign before committing cluster time.

It is not a guard against the two destructive processes: 20 (clean-scan, which deletes wavefunctions and moves the run to completed/) and 28 (pressure-reset, which removes the pressure/ tree) delete whatever the start/end range in input.in selects, --dry-run or not. Check the range before running either.

Job ids

Jobs are submitted with sbatch --parsable and the returned ids are written to .htesp_job.json in the stage directory:

{"ph": [{"job": 4831201, "time": 1758067200}]}

mainprogram checkph reads those ids and asks squeue -h -o %i -j <ids> whether they are still queued. The old check grepped the whole queue for the compound name, so a job named B matched every job in the queue containing a B, and elemental compounds were reported as running forever. A stage whose job ids were not recorded, or a machine with no squeue, is now reported as unverifiable rather than silently passed.

The same file is what the tutorial runner polls when it waits for a step to finish.

Failure accounting

Each material’s result carries its own status. A stage in which every material failed exits 1; a malformed input.in or an unknown command exits 2; Ctrl-C exits 130. In HTESP 1.x every stage exited 0, because the bash layer discarded the exit code of each helper it called, so a campaign could fail completely while every && in a job script carried on.