Backend contract#
The ComputeBackend ABC — the where half — plus the backend registry.
The ComputeBackend contract: six methods every execution target implements.
Interface only – no subprocess, no SSH, nothing executable lives here. The
concrete backends plug in behind it: local (subprocess, Phase 1.4) and
ssh_exec (SSH / SLURM, Phase 4), looked up by kind through
registry.py. Keeping the interface free of any execution detail is the
“separate what from where” thesis applied to the compute plane: a stage builds a
portable JobSpec and never learns which machine ran it; making the screening
stage run on a remote GPU box is a config change, not a code change.
Adding a backend is one new subclass plus one register(kind, cls) call –
no stage, orchestrator, or CLI change. If adding a backend seems to require
touching a stage, the abstraction is leaking: stop.
- class matmelt.backends.base.ComputeBackend(name, options)[source]#
Bases:
ABCOne uniform interface over every execution target.
Lifecycle contract – the order a driver calls these in:
capabilities() / health_check() # before any work is handed over handle = submit(spec) # non-blocking: work starts, handle returned while not poll(handle).is_terminal: ... result = fetch(handle) # faithful exit code + streams + outputs release(handle) # idempotent cleanup, always (finally)
Invariants every implementation must hold (do not weaken):
Two failure tiers. A job that runs but exits non-zero is a normal
JobResultwith a non-zeroexit_code– NOT an exception. Only infrastructure failures (unreachable target, unknown handle, malformed spec) raiseBackendError. Judging scientific success is the stage’s job; the backend only reports faithfully what happened.Handles are durable. A
Handlemust keep resolving after the tool restarts (native id + persisted paths, never a live object), so an interruptedmatmelt runre-polls its in-flight jobs instead of orphaning or double-submitting them.``health_check`` never raises. Being down is the normal answer it reports.
No hardcoded compute details. Hosts, paths, queues, credentials come from the
optionsdict a backend is constructed with (config /env:NAMEreferences) – never a string literal in backend code.
- abstract 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
- abstract submit(spec)[source]#
Start the work described by
spec; return a durableHandleimmediately.Non-blocking: stages
spec.input_artifactsas the target requires (in place locally, uploaded remotely), launches the command, and returns – progress is observed throughpoll, never by waiting here.- Parameters:
spec (
JobSpec) – The portable job description built by a stage.- Returns:
Durable identity for the started job, safe to persist and re-resolve after a restart.
- Return type:
Handle- Raises:
BackendError – Only when the backend cannot accept the job: unreachable target, malformed spec, an input it cannot stage. A job that starts and then fails is reported through
poll/fetch, never raised here.
- abstract poll(handle)[source]#
Return the current
JobStatusof the job behindhandle.Cheap and idempotent: called repeatedly until terminal, and calling it again after that keeps returning the same terminal status. Must work from the handle’s persisted identity alone (no in-memory job map required), so a restarted tool can poll jobs it did not launch.
- Parameters:
handle (
Handle) – The handlesubmitreturned, possibly reloaded from the state store’s in-flight ledger in a later process.- Returns:
Where the job is now.
- Return type:
JobStatus- Raises:
BackendError – When the handle is unknown to this backend, or the status genuinely cannot be obtained.
- abstract fetch(handle)[source]#
Collect a finished job’s result: exit code, streams, output artifacts.
Meaningful once
pollreports a terminal status.- Parameters:
handle (
Handle) – The handle of a job that has reached a terminal status.- Returns:
The faithful outcome.
exit_codeis reported, never raised: non-zero is returned as-is, andNonemeans “ran, code unknown” and must never be defaulted to0.output_artifactsholds the files named inJobSpec.output_artifactsthat were actually found – a missing one is absent from the dict, for the stage to judge.- Return type:
JobResult- Raises:
BackendError – Only when the result itself cannot be retrieved: unknown handle, lost job directory, unreachable target.
- abstract health_check()[source]#
Report whether this backend can take work right now. Never raises.
The driver checks health before handing a backend any work and simply skips a sick backend until it recovers.
- Returns:
Being down is the normal answer:
HealthStatus(ok=False, detail=...)with a short, human-readable, secret-free reason for logs anddoctoroutput.- Return type:
HealthStatus
- abstract release(handle)[source]#
Clean up whatever the backend holds for
handle. Idempotent.Called by the driver of the job lifecycle in a
finallyblock (afterfetch, or when abandoning a job): remove scratch dirs, kill a still-live process, forget bookkeeping. Never judges or records the job’s result; that is the caller’s job.
The backend registry: kind string -> ComputeBackend class.
The single seam where config becomes a running backend. A user’s YAML names
each backend only by kind (local, ssh_exec, …); this module maps
that string to the concrete class and instantiates it. Because the lookup is by
string, adding a backend is one subclass + one register call – no stage,
orchestrator, or CLI change.
Registration is an explicit call, not an import-time decorator, so there is
no hidden import-order dependency: a concrete backend module calls
register("local", LocalBackend) at the bottom of its file, and whoever wires
the tool imports the backend modules it intends to offer.
Construction contract: a registered class is instantiated as
cls(name, options) – the backend’s configured name plus its free-form
options dict. The dict (rather than unpacked kwargs) keeps the contract stable
as backends grow new options; each backend validates what it needs in its own
__init__ and raises BackendError for a missing/invalid option.
- matmelt.backends.registry.register(kind, cls)[source]#
Register
clsas the implementation ofkind.Called explicitly at the bottom of each concrete backend module.
- Parameters:
kind (
str) – The string a config’sbackends.<name>.kindnames.cls (
type[ComputeBackend]) – The implementing class, which must be aComputeBackendsubclass.
- Raises:
BackendError – If
clsis not aComputeBackendsubclass, or ifkindis already registered to a different class. Re-registering the same class is idempotent, so a module re-import is a no-op, but silently shadowing an existing kind is refused.- Return type:
- matmelt.backends.registry.get(kind)[source]#
Return the class registered under
kind.- Parameters:
kind (
str) – The backend kind to look up.- Returns:
The registered
ComputeBackendsubclass.- Return type:
- Raises:
BackendError – If nothing is registered under
kind. The message names the kinds that are known, so a config typo produces an actionable message rather than aKeyError.
- matmelt.backends.registry.create_backend(name, kind, options)[source]#
Instantiate the backend
nameofkindwith itsoptionsdict.The one seam where a config block becomes a live backend.
- Parameters:
- Returns:
The constructed, option-validated backend.
- Return type:
ComputeBackend- Raises:
BackendError – If
kindis unknown, or the backend’s own__init__validation rejects an option – a missing required value or a bad one.
Examples
>>> from matmelt.backends import local # registers kind "local" >>> backend = create_backend("laptop", "local", {"max_parallel": 4}) >>> backend.name, backend.capabilities().max_parallel ('laptop', 4)