Execution hygiene
A number that exists only in a terminal scrollback, or that came from a run on a machine with more cores and memory than the one that will re-execute the pipeline, is not a result yet. The habits below cost minutes and remove the two most common ways an analysis fails after it "worked": it cannot be rerun, or it runs differently.
Rules
- Detach anything that outlives a shell prompt. A step that may exceed a few minutes
goes through
compute_job start(name, purpose, command, cwd, declared artifacts and checkpoint path), thencompute_job wait; readcompute_job logson failure. Never a shellsleeploop polling a PID or a file: blocked shells get killed with the session, the job record does not, and the job keeps its logs and exit code. - Checkpoint and resume. Persist progress at natural boundaries (per epoch, per chunk,
per input file) under the declared checkpoint path, and make the entry point look for
that state before starting. An interrupted rerun should do the remaining work, not all
of it. Name checkpoints by step (
chunk_0042.parquet), not by timestamp. - Seed everything and cap every thread pool.
random.seed(s),np.random.default_rng(s),torch.manual_seed(s), andPYTHONHASHSEED=sin the environment. ExportOMP_NUM_THREADS,MKL_NUM_THREADS,OPENBLAS_NUM_THREADSandNUMEXPR_NUM_THREADSbefore the interpreter starts (BLAS reads them at load time), and calltorch.set_num_threads(n)inside;threadpoolctl.threadpool_limits(n)caps a BLAS that is already loaded. Two reasons: reductions change summation order with thread count, so bitwise-identical reruns need a fixed count; and a BLAS that spawns 64 threads inside a 2-CPU quota thrashes. On Linux readlen(os.sched_getaffinity(0)), notos.cpu_count(), and check/sys/fs/cgroup/cpu.maxandmemory.maxfor the quota. Full GPU determinism needstorch.use_deterministic_algorithms(True)andCUBLAS_WORKSPACE_CONFIG=:4096:8; say when you accept nondeterminism instead. - Stream large tables and declare dtypes.
pd.read_csvwithusecols=[...],dtype={...}andchunksize=200_000, aggregating per chunk;np.load(path, mmap_mode="r")ornp.memmapfor arrays;pyarrow.parquet.ParquetFile(path).iter_batches(batch_size=...)for Parquet;polars.scan_csvorscan_parquetfor lazy pipelines. Inferred dtypes flip between chunks (an integer column becomes float once a missing value appears), so explicit dtypes make the read deterministic and usually cut memory several-fold. - Write outputs atomically. Write to a temporary file in the destination directory,
flush and
os.fsync, thenos.replace(tmp, final). The rename is atomic only on the same filesystem, which is why the temp file lives next to the target. A reader never sees a half-written CSV, and a crash leaves either the old file or the new one. - Target the environment that will re-execute, not this one. Write down its CPU
count, memory, wall-clock limit and whether it has network. Test under those limits:
taskset -c 0-1,ulimit -v,timeout 30m, or a container with--cpusand--memory; run once with network disabled (unshare -non Linux, or unset proxies and confirm nothing downloads). Pin dependency versions; nopip installor model download at run time when the target is offline; vendor small resources next to the code. - Log commands with exit codes. Scripts start with
set -euo pipefail; each command that contributes to a reported number is appended to a run log with its exit code and duration (cmd 2>&1 | tee -a run.log; echo "exit=${PIPESTATUS[0]}" >> run.log). A silent failure in step three is the usual explanation for a wrong number in step nine. - Rerun clean before finishing. Delete derived outputs or use a fresh directory, run the final pipeline from raw inputs to final artifacts once more under the target limits, and compare with the numbers you are about to report. Time it. If it exceeds the budget, that is a defect to fix, not a footnote. Skip only when the run is measured in hours and say so.
- Never leave placeholders. No
TODOvalues, noNaNwhere a number is due, no empty files created to be filled later. When a value cannot be computed, the output states why in the place the value would appear, and the report says the same.
Snippets
import os, tempfile
def write_atomic(path: str, data: bytes) -> None:
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".", prefix=".tmp-")
with os.fdopen(fd, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno()) # rename alone does not force the bytes to disk
os.replace(tmp, path) # same filesystem, so the swap is atomic
# Thread caps must be exported before Python imports numpy or torch.
export OMP_NUM_THREADS=2 MKL_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 NUMEXPR_NUM_THREADS=2
export PYTHONHASHSEED=0
timeout 30m taskset -c 0-1 python pipeline.py --seed 0 2>&1 | tee -a run.log
echo "pipeline exit=${PIPESTATUS[0]}" >> run.log
Workflow
- Target limits written down (CPU, memory, wall-clock, network).
- Seeds and thread variables set at the top of the entry point.
- Long step dispatched with
compute_job start, thenwait; artifacts declared. - Large inputs read in chunks with explicit dtypes; peak memory measured
(
/usr/bin/time -von Linux,resource.getrusagein Python). - Every output written through a temp file and
os.replace. -
run.logholds each command with its exit code and duration. - Clean end-to-end rerun under the target limits; numbers match the report.
- Every output file is non-empty and was parsed back once before delivery.
Sources
- Python
os.replaceandos.fsync: https://docs.python.org/3/library/os.html - pandas
read_csv(dtype,usecols,chunksize): https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html - NumPy
memmapandload(mmap_mode=): https://numpy.org/doc/stable/reference/generated/numpy.memmap.html - PyTorch reproducibility notes: https://pytorch.org/docs/stable/notes/randomness.html
- OpenMP environment variables (
OMP_NUM_THREADS): https://www.openmp.org/spec-html/5.0/openmpch6.html - threadpoolctl: https://github.com/joblib/threadpoolctl
- Linux cgroup v2 (
cpu.max,memory.max): https://docs.kernel.org/admin-guide/cgroup-v2.html - Bash
setbuiltin andPIPESTATUS: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html