Tutorials
These examples assume you already have a Flux session and the matensemble package importable in
that environment in order to run (Installation).
Here we will go over what chores are and how to create them, how to build a workflow, and some capabilities that MatEnsemble gives you.
Minimal executable workflow
MatEnsemble is structured around Pipeline
The Pipeline object will create a Directed Acyclic Graph (DAG) of these chore objects and once you submit the graph to the manager it will sort the graph topologically and start the execution loop.
Pipeline.exec records a Chore.
The command is either a string or an argv list.
1from matensemble.pipeline import Pipeline
2
3pipe = Pipeline()
4
5pipe.exec(command=["echo", "hello from MatEnsemble"])
6
7pipe.submit()
Nothing runs until submit(), which builds the DAG,
instantiates FluxManager, and enters the scheduling loop.
Parameters you will commonly set on exec():
num_tasks— Flux task count (for MPI programs this is usually your rank count).cores_per_task/gpus_per_task— resource hints for scheduling.mpi=True— togglesmpi=pmi2on the Flux jobspec; your program must initialize MPI accordingly.env/inherit_env— see Configuration and behavior reference.
PYTHON Chores and OutputReference
The other type of chores that you can create with MatEnsemble are PYTHON chores. These are still a unit of work for MatEnsemble to manage the state and execution, but rather than a call to an external executable, PYTHON chores are delayed function calls. You can define your own python functions and add those as chores for the manager to handle.
The Pipeline has a decorator function that you can use to register a function.
When you instantiate a function it does NOT add any chores the the Pipeline yet.
Decorated functions are not executed immediately. When you call a PYTHON Chore
it returns a OutputReference placeholder.
Defining chores
1from matensemble.pipeline import Pipeline
2from mpi4py import MPI
3
4# We first create a Pipeline and define an MPI-enabled chore that launches
5# 10 parallel MPI ranks using mpi4py.
6pipe = Pipeline()
7
8
9# Next we register a function to MatEnsemble
10@pipe.chore(num_tasks=10, cores_per_task=1, gpus_per_task=0, mpi=True)
11def mpi_hello_world():
12 size = MPI.COMM_WORLD.Get_size()
13 rank = MPI.COMM_WORLD.Get_rank()
14 name = MPI.Get_processor_name()
15
16 print(f"Hello World! I am process {rank} of {size} on {name}.")
17
18
19# Then we create 10 Chore objects by calling the registered function
20for _ in range(10):
21 mpi_hello_world()
22
23# Submit the workflow with the logger refreshing every second
24pipe.submit(log_delay=1)
Chaingin PYTHON Chores
OutputReference
objects can be treated as the results of a PYTHON chore and passed to other calls to PYTHON chores. When you pass
an OutputReference to another chore call MatEnsemble will create an edge between those
two chores and when you submit the workflow MatEnsemble will see the downstream PYTHON chore as a dependent and will
ensure that these jobs are submitted in the correct order.
# functions.py
from matensemble.pipeline import Pipeline
pipe = Pipeline()
@pipe.chore()
def chore1():
return 1
@pipe.chore()
def chore2(x):
return x + 1
@pipe.chore()
def chore3(x):
return x * 2
a = chore1()
b = chore2(a)
c = chore3(b)
pipe.submit()
FluxManager only schedules chore2 after chore1 finishes, and chore3 after
chore2 finishes. Internally, the worker deserializes ../chore1/result.pickle before invoking chore2.
Note
Cycles are rejected during DAG validation. Fan-in (many tasks → one consumer) and fan-out are supported so long as the graph remains acyclic.
User Defined Strategies
MatEnsemble uses the strategy design pattern for the processing of chore completions. There are two
internal strategies that are shipped automatically. AdaptiveStrategy
and NonAdaptiveStrategy. The AdaptiveStrategy
Users can also define their own strategies to be injected into the manager at runtime. MatEnsemble
provides another decorator to do this.
from matensemble.model import Resources
from matensemble.pipeline import Pipeline
from matensemble.chore import ChoreSpec
pipe = Pipeline()
screen_resources = dict(num_tasks=1, cores_per_task=1)
validation_resources = dict(num_tasks=1, cores_per_task=4)
@pipe.chore(name="screen_candidate", **screen_resources)
def screen_candidate(candidate):
"""Cheap proxy for a simulation or surrogate-model evaluation."""
temperature = candidate["temperature"]
return {
"candidate": candidate,
"formation_energy": ((temperature - 1500) ** 2) / 1_000_000,
}
@pipe.chore(name="analyze_screen", **screen_resources)
def analyze_screen(screen):
"""Decide whether this candidate deserves a more expensive validation."""
energy = screen["formation_energy"]
return {
"candidate": screen["candidate"],
"formation_energy": energy,
"uncertainty": 0.12 if energy < 0.03 else 0.02,
}
@pipe.chore(name="validate_candidate", **validation_resources)
def validate_candidate(candidate):
"""Placeholder for a larger MD, DFT, or phase-field validation run."""
return {"candidate": candidate, "validation_status": "submitted"}
@pipe.strategy(bolo_list=["analyze_screen"], **screen_resources)
def request_validation(report):
"""Spawn validation only for uncertain, high-value candidates."""
if report["uncertainty"] <= 0.05:
return None
return ChoreSpec(
args=(report["candidate"],),
kwargs={},
resources=Resources(**validation_resources),
qualname="validate_candidate",
)
for temperature in (1400, 1500, 1700):
candidate = {"composition": "SiO2", "temperature": temperature}
screen = screen_candidate(candidate)
analyze_screen(screen)
future = pipe.submit(log_delay=1)
print(future.result())
The strategy() can be thought of as adding a callback to a
Chore. This function has access to the internal
FluxManager queue. If this strategy function returns a
ChoreSpec then it will be added to the matensemble queue at runtime.
This lets you create workflows that expand dynamically.
The bolo_list is the list of chore names that should trigger the strategy. If one of these chores completes,
MatEnsemble launches the strategy and passes the completed chore result as an argument.
User-defined strategies can observe completed chores and dynamically add more work by returning
ChoreSpec objects.
Nested arguments
Dependency scanning walks nested containers and non-class dataclasses. You may pass structured payloads
mixing plain data and OutputReference instances; the worker recursively replaces
references with concrete Python objects.
Third-party imports inside chores
Because workers import the defining module in full, top-level imports run automatically. You do not need
to bury import numpy inside the chore body unless you want lazy loading for side-effect control.
If you need extra wheels:
Containers: extend the provided image (Apptainer
%postsnippet withuv pip install …, Installation).Virtualenv on NFS: install once into the environment shared by all nodes.
Further reading
Configuration and behavior reference — complete
submit()parameter table and artifact schemas.API — authoritative signatures mirrored from the source docstrings.