Many sweeps through few arrays
A sweep is one run directory, one manifest, and one array. This approach doesn’t necessarily scale well to a full campaign that may contain hundreds of them, since one array per sweep can quickly reach the scheduler’s per-user job limit.
research_helpers.sweep.queue decouples sweeps from arrays. The unit of work becomes a slice
of a sweep (i.e., what a single array task effectively runs). A queue is a text file of such units,
and each element of one array runs a block of them, in sequence. Nothing about a sweep changes: every
slice still writes its own part file into its own run directory, resuming still skips finished combinations,
and status and collect read each run directory as normal.
The commands below are on every Sweep’s command line, so the
sweep_demo.py from the sweeps guide has them already.
Plan the campaign
A plan list names each sweep’s config and run directory, one tab-separated pair per line, with
blank lines and # comments allowed:
configs/g01-fast.toml runs/g01-fast
configs/g01-slow.toml runs/g01-slow
configs/g02-fast.toml runs/g02-fast
...
plan-list plans all of them in one process.
$ python sweep_demo.py plan-list --list PLAN.tsv
planned 6 sweeps: 24 combinations in 12 slices
queue them with the queue command, rather than submitting one array per sweep
Write the queue
$ python sweep_demo.py queue --run-dir 'runs/*' --where method=slow --out queues/slow --per-task 3
3 sweeps, 6 units, 3 per array element, 1 queue file(s)
submit with:
sbatch --array=1-2 <your queue script> queues/slow.01.units 3
Each line of the file is one unit, a run directory, and a slice index:
$ head -3 queues/slow.01.units
runs/g01-slow 1
runs/g01-slow 2
runs/g02-slow 1
Naming sweeps.
--run-dirtakes paths and quoted globs, expanded by the command rather than the shell, so thousands of run directories can fit in a single argument.--listtakes plan lists instead.Splitting a campaign. Slices of different kinds can differ in cost by an order of magnitude, so queue them separately, each with its own block size and wall-time.
--where KEY=VALUEkeeps the sweeps whose manifest records that value, in itsmetadataor itsconstants, and--where-not KEY=VALUEdrops them. Both repeat.Block size.
--per-taskis how many units each element runs. It is part of the queue: element i runs lines (i − 1)·K + 1 to i·K, so submit with the same numberqueueprinted.Array width. A queue longer than one array allows is split across several files, each printed with its own
sbatchline. The width comes frommax-arrayunder[tool.research-helpers.sweep], 1,000 by default, or--max-array.Mixing.
--shuffle SEEDputs the units in a seeded random order, so each element’s block is a sample of the whole campaign. Element wall-times then gather around the mean instead of inheriting the slowest sweep’s. The same seed always gives the same queue.
Run it
Copy queue.sbatch out of the package (cluster jobs), set its SWEEP_MODULE, and
submit the line queue printed. Each element calls run-units, which runs its block:
$ python sweep_demo.py run-units --units queues/slow.01.units --per-task 3 --element 1
element 1: 3 unit(s) from queues/slow.01.units
=== runs/g01-slow task 1 ===
[task 1/2] combinations 0..1 (2 assigned, 0 already done, 2 to run)
[task 1] 1/2 5024202ad27b in 0.0s
[task 1] 2/2 7bc25422bde8 in 0.0s
wrote runs/g01-slow/parts/task-00001.jsonl
...
Under the scheduler the element comes from SLURM_ARRAY_TASK_ID, as a task’s index does for
run. A unit that raises is reported and the rest of the block still runs. The element then
exits non-zero, which the scheduler records as a failure.
The @sweep.context factory runs once per unit, never once per element to avoid a shared
context carrying one slice’s state into another’s results.
Resume
status takes globs too, and reports one line per sweep:
$ python sweep_demo.py status --run-dir 'runs/*-slow'
runs/g01-slow 4/4 100.0%
runs/g02-slow 2/4 50.0% unfinished slices 2
runs/g03-slow 0/4 0.0% unfinished slices 1-2
1 of 3 sweeps finished
queue the unfinished slices with queue --pending, under a new --out
--pending queues only the slices that still hold an unfinished combination. Each unit names a slice
by the index its manifest planned, so nothing is re-partitioned and the resumed queue runs exactly
the slices that are missing.
$ python sweep_demo.py queue --run-dir 'runs/*-slow' --out queues/slow_r1 --per-task 3 --pending
3 sweeps, 3 pending units, 3 per array element, 1 queue file(s)
submit with:
sbatch --array=1-1 <your queue script> queues/slow_r1.01.units 3
Warning
Write a resumed queue under a new stem, and only once the old array has finished. An element
reads its block from the file when it starts, so rewriting a queue that still has pending elements
hands them another queue’s blocks, meaning that some slices would run twice, while others would never run.
queue refuses to overwrite an existing queue unless given --force:
$ python sweep_demo.py queue --run-dir 'runs/*-slow' --out queues/slow --per-task 3
queues/slow.01.units exists. Give this queue a new stem (e.g. slow_r1), or rewrite it with --force (force=True) only once no array is still reading it
collect over many sweeps collects each one, and exits non-zero if any has no results yet.
Elements that ran twice
When a node stops answering, a scheduler can re-queue an element while the original is still running.
The block then runs twice, from two nodes, both appending to the same part files, and on network
storage that leaves runs of NUL bytes and lines spliced together. The damage is not confined to the
lines collect reports as malformed since a valid line written after the NULs is linked to them, and a
resumed task appends its new line to the damaged file, also linked, so the slice never
finishes.
Both array templates set --no-requeue to prevent this. If it happens anyway, set the elements’
results aside whole, part files and artefacts, and queue them again:
$ python sweep_demo.py quarantine --units queues/slow.01.units --per-task 3 --elements 1
1 element(s) of queues/slow.01.units: 3 units, 3 part files, 0 damaged part files, 0 artefacts
nothing changed. With --apply, the part files and artefacts are renamed *.quarantined
Without --apply, the command only counts. With --apply, every file is renamed with a .quarantined
suffix and queue --pending then finds the slices missing. Recomputing them reproduces the lost results
only as far as the evaluation is deterministic, so rerun them on the same kind of node as the rest of
the campaign.