Orchestration#
Run experiments over parameter ranges, persisting their metrics.
The user writes an experiment as a single run, sweeps it with ordinary Python
loops, and reads the results back per metric as a tidy
MetricTable, which holds plain Python
lists and converts to whichever dataframe library the reader prefers.
Each run is a job, identified by its parameters and by the code it executes
(see krum.orchestration.hashing), and owning a folder under the
orchestrator’s root (see krum.orchestration.storage). A job is
re-executed only when it has no recorded result, when its last attempt failed,
when the environment it was recorded in no longer matches, or when one of the
functions it called has since changed; otherwise its stored output stands.
Orchestrator.plan reports what a sweep would do, and why, without running
any of it.
Execution is synchronous and fail-fast: the first failing job stops the sweep, and the remaining jobs stay queued. One process per job, and re-running a job whose dependencies changed, are the next two steps in notes/adr-2026-10-02-orchestrator-v2-a-design.md.
Example:
from krum.orchestration import Metric, Orchestrator
from krum.primitives.aggregators.average import Average
from krum.primitives.aggregators.krum import Krum
from krum.primitives.attacks.alie import ALIEAttack
from krum.primitives.attacks.sign_flip import SignFlipAttack
def my_experiment(n, f, aggregator, attack, seed):
simulation = ... # e.g. a krum.simulations class
loss = Metric("loss", dtype=float)
for step in range(100):
simulation.step()
if step % 10 == 0:
test_loss, _accuracy = simulation.evaluate()
loss.push(step, test_loss)
with Orchestrator("byzantine_study") as orch:
for n, f in [(10, 2), (20, 3)]:
for aggregator in [Average, Krum]:
for attack in [ALIEAttack, SignFlipAttack]:
orch.run(my_experiment, n=n, f=f, aggregator=aggregator, attack=attack, seed=42)
loss = orch.get("loss") # columns: step, value, n, f, aggregator, attack, seed, job_key
loss["value"] # one column, as a list
frame = loss.to_pandas() # or: polars.DataFrame(loss.to_dict())
frame.groupby(["aggregator", "attack", "step"])["value"].mean()