Execution#
How a job’s body is executed: in this process, or in one of its own.
Running each job in a fresh interpreter is a reproducibility measure before it is a performance one. Nothing a job leaves behind — a seeded global generator, an imported module’s mutable state, an initialised accelerator context — can reach the next job, so a sweep’s results do not depend on the order its jobs happened to run in.
It costs two requirements, which is why it is opted into rather than assumed:
The experiment and its parameters must be picklable, since they are sent to the child. A function or class is pickled by name, so it has to be reachable at module level rather than defined inside another function.
A sweep script’s top level must be guarded by if __name__ == “__main__”:. A spawned child re-imports the module it came from, and an unguarded sweep would start itself again there.
See notes/adr-2026-10-02-orchestrator-v2-a-design.md.
- class krum.orchestration.execution.Executed(error: str | None = None, exception: BaseException | None = None, called: dict[str, str] | None = None)[source]#
Bases:
objectThe outcome of running one job’s body.
- property called: dict[str, str] | None#
The functions the job entered, or None if it was not traced.
- property exception: BaseException | None#
The exception itself, for a job that ran in this process.
- class krum.orchestration.execution.InlineRunner(owned: Iterable[str] = (), trace: bool = False)[source]#
Bases:
objectRuns each job in the orchestrator’s own process.
- Parameters:
owned – Module prefixes whose functions are worth recording.
trace – Record which owned functions each job entered.
- class krum.orchestration.execution.SubprocessRunner(owned: Iterable[str] = (), trace: bool = False, workers: int = 1)[source]#
Bases:
objectRuns each job in a fresh interpreter of its own.
The pool is spawned rather than forked, and retires each worker after one job, so no two jobs share an interpreter. Raising workers above one is all that stands between this and running a sweep in parallel; it is left at one for now, the orchestrator still being fail-fast and sequential.
- run(writer: JobWriter, callable: Callable[[...], Any], params: Mapping[str, Any]) Executed[source]#
Execute a job’s body in a child process and collect what it recorded.
A child that dies outright, rather than raising, leaves the pool unusable; it is discarded so that the next job starts from a fresh one, and the death is reported as that job failing.
- krum.orchestration.execution.execute_job(path: str | PathLike[str], callable: Callable[[...], Any], params: Mapping[str, Any], owned: tuple[str, ...] = (), trace: bool = False) dict[str, Any][source]#
Run one job’s body, recording its metrics into an already staged directory.
This is the child process entry point, so it is a module-level function that returns rather than raises: an exception’s traceback does not survive being sent back to the parent, so the traceback is formatted here, where the frames still exist.
- Parameters:
path – The staged directory to record into.
callable – The user-defined function to execute.
params – The parameters to execute it on.
owned – Module prefixes whose functions are worth recording.
trace – Record which owned functions the job entered.
- Returns:
The formatted traceback if any, the metrics that were registered, and
the functions entered when tracing was asked for.