Built-in backends#
Running jobs on this machine, or on a machine you own over SSH.
The local backend: run a JobSpec as a subprocess on this machine.
The zero-setup default – and the reference implementation of the durability
contract. Registered under kind = "local" at import time.
Execution model (the why behind the shape):
submitis non-blocking: it stages inputs into a fresh per-job directory, launches the command through a tinyjob.shwrapper, and returns aHandleimmediately. Progress is observed viapoll; results viafetch.The ``.rc`` sentinel is the durability mechanism. The wrapper runs the command, then atomically writes its exit code to
job.rc(tmp-file +mv). Everythingpoll/fetchneed afterwards lives on disk (job.rc,stdout.log/stderr.log, ajob.jsonmanifest), so a restarted tool resolves the same handle with no in-memory state: rc file present -> terminal with a faithful code; rc absent + process alive ->RUNNING; rc absent + process gone ->FAILEDwithexit_code=None(“ran, code unknown” – never defaulted to 0).PID reuse is defeated by a start-time check. The handle records the wrapper’s PID and its
/proc/<pid>/statstart time; a live PID only counts as our job if the start time still matches (best-effort: on hosts without/procthe check degrades to PID-aliveness).Two failure tiers. A malformed spec raises
BackendError(programming error, nothing launched). A job-level problem is never raised: a missing input artifact becomes a durable already-failed job (detail instderr.log, no process), and a command-not-found simply exits 127 through the wrapper – one bad item fails while the backend stays healthy.
Stdlib-only (subprocess/pathlib/shutil/shlex/tempfile).
No compute detail is hardcoded: work_dir/gpu/max_parallel come from
the backend’s configured options.
- class matmelt.backends.local.LocalBackend(name, options)[source]#
Bases:
ComputeBackendRun jobs as child processes on this machine.
The default backend, and the one that needs no setup: a bare
{kind: local}works.- Parameters:
name (
str) – The backend’s configured name, carried into everyHandleit issues.The config block. All keys are optional:
work_dirBase directory for per-job directories. Defaults to a private temp directory – zero setup, but those job directories die with the OS temp cleaner, so configure a persistent path to keep in-flight jobs resumable across restarts.
gpuWhether this host exposes a GPU to jobs. Default
False, and config-driven rather than auto-detected, so capabilities stay predictable.max_parallelHow many jobs this backend advertises it can run at once. Default
1; advertised here, enforced by the orchestrator.
- Raises:
BackendError – For an option of the wrong type or an unusable
work_dir.
Notes
Durability is what distinguishes this from a plain
Popenwrapper. Each job gets a directory holding a wrapper script, an atomically-written exit-code sentinel, both captured streams, and a manifest of what the job promised to produce.pollandfetchread only those, so a restarted tool resolves jobs it never launched – no in-memory map is required, and an interrupted run resumes rather than orphaning its work.- capabilities()[source]#
Report what this backend offers (cpu/gpu/slurm,
max_parallel).Pure introspection – nothing is submitted, nothing mutates. A stage’s
Resourcesneeds are asserted against this before any work is handed over, so an impossible placement (a GPU stage on a CPU-only backend) is caught up front rather than as a late job failure.- Returns:
What this target offers.
max_parallelis advertised here and enforced by the orchestrator, not the backend.- Return type:
Capabilities
- health_check()[source]#
Probe that the job root is writable. Never raises.
- Returns:
Whether a file can be created and removed under
work_dir, with the reason when it cannot.- Return type:
HealthStatus
- sample_cpu_percents(handles)[source]#
Sample CPU usage per job – the monitor’s duck-typed CPU hook.
- Parameters:
handles (
Sequence[Mapping[str,Any]]) – The ledger’s serialized handles, in order. The wrapper PID lives atextra.pid.- Returns:
One reading per handle, in the same order, as a percentage of one core. Readings come from the shared
LocalCpuTracker–/proctick deltas over each job’s whole process subtree – so the first call after startup answersNonefor every job, that being the delta baseline. A handle without an integer PID, or a PID no longer running, is alsoNone.- Return type:
Notes
Never raises: this is cosmetic telemetry, and its contract is that a broken reading can never take the dashboard down.
- sample_gpu_percents(handles)[source]#
Sample GPU usage per job – the monitor’s duck-typed GPU hook.
- Parameters:
handles (
Sequence[Mapping[str,Any]]) – The ledger’s serialized handles, in order.- Returns:
One SM-utilisation reading per handle, summed over that job’s process subtree. A machine without
nvidia-smi, or one where it fails or times out, answers allNone– unknown, never zero. A live job with no pmon entry is an honest0.0: running, but not on the GPU.- Return type:
Notes
One
nvidia-smi pmon -c 1 -s ucall answers the whole batch. Never raises.
- submit(spec)[source]#
Start
specthrough a shell wrapper and return a durable handle.- Parameters:
spec (
JobSpec) – The job to run. Itscwdis the material’s directory, so outputs land beside the science data;scratch_inputsstage into this backend’s private job directory instead, and the matching command token is rewritten to point there – which is what keeps runner scripts out of material directories.- Returns:
Carrying the job directory, the wrapper PID, and that PID’s
/procstart time.- Return type:
Handle- Raises:
BackendError – Only for a structurally malformed spec, which is a programming error. A job-level problem – an unstageable input, for instance – becomes a durable already-failed job instead, whose handle still resolves: one bad item must not look like a sick backend.
Notes
The wrapper writes the exit code to a sentinel file via tmp-then-rename after the command returns. That sentinel, not the process, is what
pollandfetchbelieve – and recording the PID start time alongside the PID is what makes re-attachment safe against PID reuse.
- poll(handle)[source]#
Report status from durable evidence alone.
- Parameters:
handle (
Handle) – The job to poll, possibly reloaded from the ledger in a later process.- Returns:
DONEorFAILEDwhen the exit-code sentinel exists;RUNNINGwhen the wrapper process is alive and still ours;FAILEDotherwise – no exit code and no live process means the job never started, died before writing the sentinel, or could not write it.- Return type:
JobStatus- Raises:
BackendError – When the handle’s job directory does not exist: unknown, or already released.
Notes
Works identically for a job this instance launched and one re-attached after a restart, because it consults the sentinel and
/procrather than any in-memory state.
- fetch(handle)[source]#
Collect the faithful result from the job directory. Restart-safe.
- Parameters:
handle (
Handle) – The job to collect.- Returns:
The sentinel’s exit code –
Nonewhen absent or garbled, which means “ran, code unknown” and is never defaulted to0– both captured streams, and the declared outputs that were actually produced, resolved against the job’s recorded cwd.- Return type:
JobResult- Raises:
BackendError – When the job directory is gone, or when the job is still running and was launched by a previous process, which cannot be waited on. Poll until terminal instead.
Notes
A still-running job this instance owns is waited for, so
fetchnever returns a half-finished result.
- release(handle)[source]#
Stop the job if it is ours, then remove its directory.
- Parameters:
handle (
Handle) – The job to clean up. Releasing an unknown or already-released handle is a no-op, so restart-time cleanup is always safe.- Return type:
Notes
Idempotent and never raises. A process this instance launched is SIGTERMed then SIGKILLed; a re-attached one is killed only after the start-time check has proven the PID is still our wrapper and not a recycled stranger.
The job directory holds this backend’s scratch, including the staged runner script, so removing it is what keeps our implementation files out of the material directory – which only ever received the item’s science data and outputs.
The ssh_exec backend: run a JobSpec on a remote host over SSH (4.1).
Modelled on MVP_V0’s ssh_exec (its durable-native-id + .rc-sentinel +
single-SSH-seam shape is proven) but with the data model inverted per the
2026-07-08 decisions: MVP_V0 assumes a shared filesystem and stages nothing;
here declarative staging is the default and shared_filesystem: true is
the opt-out optimization.
Remotes never receive our package code. A remote is a pure calculation executor: the only things there are the user’s environment (
remote_pythonwith the science deps) and what we push per job. Every job’s declaredinput_artifacts– which, by the Phase-3 stage discipline, always include the stage’s self-contained runner script – are staged into a fresh per-job remote directory; the command runs there (artifact names resolve in the cwd, same aslocal); declaredoutput_artifactsare pulled back into the job’s localcwd(the item dir), where the stage judges them. Our runners are stdlib+science only, so there is no PYTHONPATH plumbing at all.The staging transport is a seam.
_stage_in/_stage_outwrap rsync-over-SSH today; an S3/Globus transport plugs in behind the same two calls with zero stage changes. Withshared_filesystem: trueboth become no-ops: the job runs inspec.cwd(the same absolute path on both sides) and outputs are collected locally, exactly likelocal.A durable native id.
Handle.job_idis the remote PID (or the SLURM job id underuse_slurm); everythingpoll/fetch/releaseneed (scheme, remote job dir, log/sentinel paths, output names, local cwd) rides inHandle.extraas plain JSON-safe strings – no in-memory job map, so a restarted orchestrator re-attaches from the persisted handle alone.Exit-code fidelity via the ``.rc`` sentinel. The remote wrapper appends
echo $? > <rc>;fetchreads it back. A missing/garbled sentinel isexit_code=None(“ran, code unknown”) – never a silent0. The SLURM path preferssacct’s ExitCode and falls back to the sentinel.
Stdlib + the system ssh/rsync (and remote sbatch/sacct) only.
Key-based auth must already work: every SSH runs BatchMode=yes and never
prompts. All compute details come from options – no host, user, path, or
queue literal in code, and health_check never leaks a secret.
- class matmelt.backends.ssh_exec.SshExecBackend(name, options)[source]#
Bases:
ComputeBackendRun jobs on a remote host over SSH, staging files per job by default.
This is the backend that makes “run the screening stage on my GPU box” a config change rather than a code change.
- Parameters:
name (
str) – The backend’s configured name, carried into everyHandleit issues.The config block. Only
hostis required:hostAn
~/.ssh/configalias, a hostname, oruser@host.userSSH user, when
hostcarries none.ssh_binary,rsync_binaryClient binaries, defaulting to
sshandrsync.remote_pythonInterpreter to substitute for
argv[0]remotely. Normally unnecessary – the stage’s ownpythonoption already names the remote interpreter – but this override wins when both are set.remote_work_dirRoot for per-job remote directories, default
/tmp/matmelt-jobs. Each job gets a fresh subdirectory, removed byrelease.shared_filesystemtrueskips data staging, assuming the remote sees the job’scwdat the same absolute path over NFS or SSHFS. Scratch inputs – the runner script – are still pushed into the per-job directory and referenced by absolute path, because they are deliberately kept out of the material directory. Defaultfalse.use_slurmSubmit through
sbatchand track the SLURM job id instead of a bare PID.partitionsets the queue, andJobSpec.resourcesbecomes--cpus-per-taskand--time. Defaultfalse.gpu,max_parallelAdvertised in
capabilities.
- Raises:
BackendError – For a missing
hostor any option of the wrong type.
Notes
Remotes never receive MatMelt’s code. A remote is a pure calculation executor: what travels is the job’s self-contained runner script plus its declared input files, and what comes back are the declared outputs. There is no
pip installon the remote, noPYTHONPATH, and no assumption that our package is importable there.Declarative staging is therefore the default, and
shared_filesystemis an optimization that skips it. Both paths run through the same two seams,_stage_inand_stage_out, so a future S3 or Globus transport needs no stage changes at all.Everything
poll,fetchandreleaseneed rides inHandle.extraas JSON-safe values – there is no in-memory job map, so a restarted run re-attaches to live remote jobs from the ledger alone. Every token crossing the SSH boundary is quoted withshlex.quote.- capabilities()[source]#
Report what this backend offers (cpu/gpu/slurm,
max_parallel).Pure introspection – nothing is submitted, nothing mutates. A stage’s
Resourcesneeds are asserted against this before any work is handed over, so an impossible placement (a GPU stage on a CPU-only backend) is caught up front rather than as a late job failure.- Returns:
What this target offers.
max_parallelis advertised here and enforced by the orchestrator, not the backend.- Return type:
Capabilities
- health_check()[source]#
Check that the remote is reachable and its work root writable. Never raises.
- Returns:
Down is a normal answer, reported with a short, secret-free detail. The orchestrator pauses this backend’s stages on a down report and resumes them by itself once it recovers.
- Return type:
HealthStatus
- property probe_target: str#
Where this backend’s remote probes run.
- Returns:
The SSH target.
doctorreports it as the location of a readiness answer, and builds the ssh-prefixed install command from it – an answer is only as good as where it was obtained.- Return type:
- check_remote_import(python, module)[source]#
Ask whether the remote interpreter can import a module. Never raises.
Closes the preflight’s blind spot for remotely-wired stages: the science stack must exist in the interpreter that will actually run the staged runner, not in ours.
- Parameters:
- Returns:
(satisfied, detail). Transport failure, a missing remote interpreter, and a failing import all come back as(False, why).- Return type:
Notes
Duck-typed rather than part of the six-method ABC:
core/preflightlooks for this hook, so any future remote backend opts in simply by providing it andprobe_target.
- check_remote_file(path)[source]#
Ask whether a path exists as a regular file on the remote. Never raises.
The file-existence analogue of
check_remote_import. A stage option naming a path – the screen stage’smodel_path– points at where the stage runs, so stat-ing our own disk would report a false problem and could refuse a perfectly valid run.
- sample_cpu_percents(handles)[source]#
Sample remote CPU usage per job – the monitor’s duck-typed CPU hook.
- Parameters:
handles (
Sequence[Mapping[str,Any]]) – The ledger’s serialized handles, in order.- Returns:
One reading per handle, summed over that job’s remote process subtree. These are
pspcpusemantics – a lifetime average rather than an instantaneous rate, which is honest enough for long-running science jobs.Nonefor a SLURM-scheme handle, where the scheduler owns the process and we hold only its job id; for a PID no longer in the table; and for any transport failure.- Return type:
Notes
One batched
psround-trip answers every handle at once, and the caller caches the result behind a TTL so a dashboard refresh does not mean an SSH round-trip. Never raises.
- sample_gpu_percents(handles)[source]#
Sample remote GPU usage per job – the monitor’s duck-typed GPU hook.
- Parameters:
handles (
Sequence[Mapping[str,Any]]) – The ledger’s serialized handles, in order.- Returns:
One SM-utilisation reading per handle, summed over that job’s remote process subtree. A remote without
nvidia-smianswers allNone– unknown, never zero – as do SLURM-scheme handles and any transport failure. A live job with no pmon entry is an honest0.0.- Return type:
Notes
One combined round-trip fetches the process table and the pmon table together, separated by a sentinel line. Never raises.
- submit(spec)[source]#
Stage inputs, launch remotely, and return a durable handle. Non-blocking.
- Parameters:
spec (
JobSpec) – The job to run.- Returns:
Carrying the remote job directory, the stream and sentinel paths, the declared outputs, the local cwd, and the tracking scheme – all JSON-safe, so the ledger alone is enough to resolve the job after a restart.
- Return type:
Handle- Raises:
BackendError – For a malformed spec, an unstageable input, an uncreatable remote directory, a failed launch, or a launch that yielded no trackable id.
Notes
Under the staged model (the default) a fresh per-job directory is created, every declared input is pushed into it, and the command runs there – so artifact names, the runner included, resolve in the cwd exactly as they do under
local.Under shared_filesystem, data inputs are not staged at all, since the remote already sees them at
spec.cwd. The scratch inputs are still pushed into the per-job directory, because the runner is deliberately kept out of the material directory, and the matching command tokens are rewritten to that absolute remote path.Either way the launch wrapper appends an exit-code sentinel, and the job is tracked by remote PID – or by SLURM job id under
use_slurm, whereJobSpec.resourcesbecomes scheduler directives.
- poll(handle)[source]#
Map the remote job’s state to a portable
JobStatus. Idempotent.- Parameters:
handle (
Handle) – The job to poll, possibly reloaded from the ledger in a later process.- Returns:
Under the PID scheme, a live process is
RUNNINGand anything else isDONE– the true outcome isfetch’s business, read from the sentinel. Under SLURM,sacctstates map conservatively, and an empty accounting line is ``RUNNING``: a job not yet visible to the accounting database must never be guessed into a terminal state.- Return type:
JobStatus- Raises:
BackendError – For a handle this backend did not issue, or transport failure.
- fetch(handle)[source]#
Collect the finished job: streams, true exit code, pulled outputs.
- Parameters:
handle (
Handle) – The job to collect.- Returns:
The exit code comes from the sentinel, or from
sacctfirst under SLURM with the sentinel as fallback. A missing or garbled sentinel isNone– “ran, code unknown”, never a silent0– and a non-zero code is reported faithfully rather than raised. Outputs are pulled back through the staging seam, or collected locally undershared_filesystem; absent ones are omitted for the stage to judge.- Return type:
JobResult- Raises:
BackendError – For a handle this backend did not issue, or transport failure.
- release(handle)[source]#
Terminate a still-running job and remove its remote directory.
- Parameters:
handle (
Handle) – The job to clean up:killunder the PID scheme,scancelunder SLURM, then the per-job directory removed.- Return type:
Notes
Idempotent and best-effort by contract. An unreachable host or an already-gone job never raises, because restart-time cleanup must always be safe. Never touches material state – that is the caller’s business.