Parsl And HPC Execution
=======================
ALF uses `Parsl `_ to submit and track workflow
tasks. Parsl does not change the scientific work ALF performs; it decides where
builder, sampler, QM, and ML tasks run, how many tasks may run concurrently, and
which scheduler allocation is requested for each task type.
This page is a practical guide for adapting ALF to a new local workstation or
HPC cluster.
How ALF Uses Parsl
------------------
ALF task functions are decorated with Parsl app definitions. Each app has one
or more executor labels. The Parsl configuration provides executors with those
labels and maps them onto local workers or scheduler allocations.
The common executor labels are:
.. list-table::
:header-rows: 1
:widths: 34 66
* - Executor label
- Typical role
* - ``alf_QM_executor``
- Runs QM labeling tasks, such as ORCA, QChem, or VASP jobs.
* - ``alf_QM_standby_executor``
- Optional lower-priority or shorter-walltime QM executor. ALF adds this
standby label automatically when it exists.
* - ``alf_ML_executor``
- Runs model training tasks.
* - ``alf_sampler_executor``
- Runs ML-driven sampling tasks.
* - ``alf_sampler_standby_executor``
- Optional standby sampler executor.
In ``master_config.json``, these fields select the Parsl configuration:
.. code-block:: json
{
"parsl_configuration": "parsl_configs.config_1node",
"parsl_debug_configuration": "parsl_configs.config_debug"
}
Normal runs use ``parsl_configuration``. Stage checks such as ``--test_builder``
and ``--test_qm`` use ``parsl_debug_configuration`` when it is present.
In the example directories, ``parsl_configs.py`` is a template for Parsl
executor, Slurm partition, account, walltime, module, and launcher settings.
The placeholders in those files must be replaced for your cluster before a
full run.
Minimal Single-Node Debug Config
--------------------------------
Use a small debug config first. It should request short walltime, few blocks,
and enough workers to run one or two tasks while you validate imports, paths,
and external executables.
.. code-block:: python
from parsl.config import Config
from parsl.executors import HighThroughputExecutor
from parsl.providers import SlurmProvider
from parsl.launchers import SimpleLauncher
config_debug = Config(
executors=[
HighThroughputExecutor(
label="alf_QM_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition="debug",
account="your_account",
nodes_per_block=1,
init_blocks=0,
min_blocks=0,
max_blocks=1,
walltime="00:30:00",
scheduler_options="#SBATCH --ntasks-per-node=1",
worker_init="module load orca",
launcher=SimpleLauncher(),
),
),
]
)
This pattern is useful for ``--test_qm`` and other bring-up checks. Replace the
partition, account, module commands, and walltime for your cluster.
One QM Job Per Allocation
-------------------------
Many QM engines are themselves parallel programs. In that case, configure Parsl
to run one ALF QM task per allocation and let the QM command use the cores or
MPI ranks inside that allocation.
.. code-block:: python
HighThroughputExecutor(
label="alf_QM_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition="standard",
account="your_account",
nodes_per_block=1,
init_blocks=0,
min_blocks=0,
max_blocks=8,
walltime="06:00:00",
scheduler_options="#SBATCH --nodes=1 --ntasks-per-node=36",
worker_init="module load orca",
launcher=SimpleLauncher(),
),
)
The important point is that ``max_workers_per_node=1`` prevents multiple QM
tasks from landing on the same node when each QM task expects the whole node.
MPI-Based DFT Codes
-------------------
For MPI-based DFT codes, the launch command usually belongs in the QM config
that ALF passes to the QM interface, while Parsl requests the allocation.
For example, a VASP-style QM config may use a command like:
.. code-block:: json
{
"QM_run_command": "srun -n 128 vasp_std"
}
The matching Parsl executor should request resources consistent with that
command:
.. code-block:: python
HighThroughputExecutor(
label="alf_QM_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition="standard",
account="your_account",
nodes_per_block=4,
init_blocks=0,
min_blocks=0,
max_blocks=4,
walltime="12:00:00",
scheduler_options="#SBATCH --nodes=4 --ntasks-per-node=32",
worker_init="module load vasp",
launcher=SimpleLauncher(),
),
)
Different clusters use different launchers and MPI integration. The resource request and QM command should agree
on node count, rank count, thread count, and environment modules.
Separate GPU Executors For ML And Sampling
------------------------------------------
ML training and MLMD sampling tasks typically need GPUs. Use separate executors so
these tasks do not compete with CPU-only (e.g., QM) jobs. Below is an example parsl config
for GPU execution of ML and sampling tasks.
.. code-block:: python
from parsl.launchers import SingleNodeLauncher
HighThroughputExecutor(
label="alf_ML_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition="gpu",
account="your_gpu_account",
nodes_per_block=1,
init_blocks=0,
min_blocks=0,
max_blocks=1,
walltime="04:00:00",
scheduler_options="#SBATCH --nodes=1 --gres=gpu:1",
worker_init="module load cuda",
launcher=SingleNodeLauncher(),
),
)
HighThroughputExecutor(
label="alf_sampler_executor",
max_workers_per_node=4,
provider=SlurmProvider(
partition="gpu",
account="your_gpu_account",
nodes_per_block=1,
init_blocks=0,
min_blocks=0,
max_blocks=2,
walltime="04:00:00",
scheduler_options="#SBATCH --nodes=1 --gres=gpu:4",
worker_init="module load cuda",
launcher=SingleNodeLauncher(),
),
)
ALF also uses ``gpus_per_node`` from the master configuration to assign visible
GPU ids inside sampler tasks.
Globus Compute Endpoints
------------------------
ALF can also target `Globus Compute `_
endpoints through Parsl. In this pattern, ``parsl_configs.py`` uses Parsl's
``GlobusComputeExecutor`` instead of a scheduler-backed executor, while keeping
the ALF executor labels such as ``alf_QM_executor``, ``alf_ML_executor``, or
``alf_sampler_executor``. This can useful for multisite runs where different endpoints
provide different resources, or for users who do not have a local scheduler.
The Parsl documentation includes a
`Globus Compute multisite example `_
with additional endpoint setup details. Below is a bare-bones Parsl configuration for using ALF with Globus Compute.
.. code-block:: python
from globus_compute_sdk import Executor
from parsl.config import Config
from parsl.executors import GlobusComputeExecutor
ml_endpoint = "YOUR_GLOBUS_COMPUTE_ENDPOINT_UUID"
config_globus = Config(
executors=[
GlobusComputeExecutor(
executor=Executor(endpoint_id=ml_endpoint),
label="alf_ML_executor",
),
],
)
For multisite runs, add more ``GlobusComputeExecutor`` entries with different
endpoint UUIDs and the ALF labels for the work each endpoint should receive.
For example, one endpoint could provide ``alf_QM_executor`` while another
provides ``alf_ML_executor`` or ``alf_sampler_executor``. Node that the Globus Compute
SDK must be installed in the Python environment that runs ALF to use this pattern functionality.
Generic CPU/GPU Template Pattern
--------------------------------
The simple-water example includes a generic Slurm template in
``examples/simple_water/parsl_configs.py``. The pattern below mirrors that file:
CPU resources are used for QM labeling, GPU resources are used for ML training
and sampling, and a smaller debug configuration is available for staged test
commands.
This is still a template. Replace partition names, accounts, QoS values,
``#SBATCH`` resource lines, module loads, launchers, and walltimes with values
that match the cluster.
.. code-block:: python
from parsl.config import Config
from parsl.executors import HighThroughputExecutor
from parsl.launchers import SimpleLauncher, SingleNodeLauncher
from parsl.providers import SlurmProvider
CPU_PARTITION = "your_cpu_partition"
GPU_PARTITION = "your_gpu_partition"
DEBUG_PARTITION = "your_debug_partition"
CPU_ACCOUNT = "your_cpu_account"
GPU_ACCOUNT = "your_gpu_account"
DEBUG_ACCOUNT = "your_debug_account"
CPU_QOS = None
GPU_QOS = None
DEBUG_QOS = None
CPU_RESOURCE_OPTIONS = "#SBATCH --nodes=1\n#SBATCH --ntasks-per-node=36"
GPU_TRAINING_RESOURCE_OPTIONS = "#SBATCH --nodes=1\n#SBATCH --gres=gpu:4"
GPU_SAMPLER_RESOURCE_OPTIONS = "#SBATCH --nodes=1\n#SBATCH --gres=gpu:4"
DEBUG_CPU_RESOURCE_OPTIONS = "#SBATCH --nodes=1\n#SBATCH --ntasks-per-node=36"
DEBUG_GPU_RESOURCE_OPTIONS = "#SBATCH --nodes=1\n#SBATCH --gres=gpu:4"
CPU_WORKER_INIT = ""
GPU_WORKER_INIT = ""
def slurm_options(extra_options="", account=None):
lines = []
if account:
lines.append(f"#SBATCH --account={account}")
if extra_options:
lines.append(extra_options)
return "\n".join(lines)
config_1node = Config(
executors=[
HighThroughputExecutor(
label="alf_QM_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition=CPU_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=2,
nodes_per_block=1,
scheduler_options=slurm_options(
CPU_RESOURCE_OPTIONS,
account=CPU_ACCOUNT,
qos=CPU_QOS,
),
worker_init=CPU_WORKER_INIT,
launcher=SimpleLauncher(),
walltime="6:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_QM_standby_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition=CPU_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=2,
nodes_per_block=1,
scheduler_options=slurm_options(
CPU_RESOURCE_OPTIONS,
account=CPU_ACCOUNT,
qos=CPU_QOS,
),
worker_init=CPU_WORKER_INIT,
launcher=SimpleLauncher(),
walltime="1:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_ML_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition=GPU_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=1,
nodes_per_block=1,
scheduler_options=slurm_options(
GPU_TRAINING_RESOURCE_OPTIONS,
account=GPU_ACCOUNT,
qos=GPU_QOS,
),
worker_init=GPU_WORKER_INIT,
launcher=SingleNodeLauncher(),
walltime="16:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_sampler_executor",
max_workers_per_node=4,
provider=SlurmProvider(
partition=GPU_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=2,
nodes_per_block=1,
scheduler_options=slurm_options(
GPU_SAMPLER_RESOURCE_OPTIONS,
account=GPU_ACCOUNT,
qos=GPU_QOS,
),
worker_init=GPU_WORKER_INIT,
launcher=SingleNodeLauncher(),
walltime="4:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_sampler_standby_executor",
max_workers_per_node=4,
provider=SlurmProvider(
partition=GPU_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=2,
nodes_per_block=1,
scheduler_options=slurm_options(
GPU_SAMPLER_RESOURCE_OPTIONS,
account=GPU_ACCOUNT,
qos=GPU_QOS,
),
worker_init=GPU_WORKER_INIT,
launcher=SingleNodeLauncher(),
walltime="4:00:00",
cmd_timeout=30,
),
),
]
)
Production executors in this template have separate roles. ``alf_QM_executor``
runs CPU QM labeling tasks with one ALF worker per node. The optional
``alf_QM_standby_executor`` can target lower-priority or shorter-walltime CPU
resources. ``alf_ML_executor`` runs one ML training task per GPU allocation.
``alf_sampler_executor`` runs MLMD or reactive sampling tasks on GPU resources,
and ``alf_sampler_standby_executor`` is the optional standby version of that
sampler pool.
The debug configuration should be smaller and faster. The ``--test_builder``,
``--test_qm``, ``--test_ml``, and ``--test_sampler`` commands use it when
``parsl_debug_configuration`` is set in ``master_config.json``.
.. code-block:: python
config_debug = Config(
executors=[
HighThroughputExecutor(
label="alf_QM_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition=DEBUG_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=1,
nodes_per_block=1,
scheduler_options=slurm_options(
DEBUG_CPU_RESOURCE_OPTIONS,
account=DEBUG_ACCOUNT,
qos=DEBUG_QOS,
),
worker_init=CPU_WORKER_INIT,
launcher=SimpleLauncher(),
walltime="2:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_ML_executor",
max_workers_per_node=1,
provider=SlurmProvider(
partition=DEBUG_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=1,
nodes_per_block=1,
scheduler_options=slurm_options(
DEBUG_GPU_RESOURCE_OPTIONS,
account=GPU_ACCOUNT,
qos=DEBUG_QOS,
),
worker_init=GPU_WORKER_INIT,
launcher=SingleNodeLauncher(),
walltime="1:00:00",
cmd_timeout=30,
),
),
HighThroughputExecutor(
label="alf_sampler_executor",
max_workers_per_node=4,
provider=SlurmProvider(
partition=DEBUG_PARTITION,
init_blocks=0,
min_blocks=0,
max_blocks=1,
nodes_per_block=1,
scheduler_options=slurm_options(
DEBUG_GPU_RESOURCE_OPTIONS,
account=GPU_ACCOUNT,
qos=DEBUG_QOS,
),
worker_init=GPU_WORKER_INIT,
launcher=SingleNodeLauncher(),
walltime="1:00:00",
cmd_timeout=30,
),
),
]
)
In these patterns, a Parsl block is one scheduler allocation. For each
executor, ``nodes_per_block * max_blocks`` controls the maximum node footprint
Parsl may request, while ``max_workers_per_node`` controls how many ALF tasks
can run on each allocated node. ALF settings such as ``parallel_samplers`` and
``target_queued_QM`` are queue-depth targets; they do not directly request
nodes. For sampler tasks, make sure ``gpus_per_node`` matches the number of
sampler workers you expect to place on each GPU node.
Configuration Knobs
-------------------
These are the Parsl settings most users need to edit.
In Parsl, a block is one scheduler allocation. For one executor, the largest
node footprint Parsl may request is ``max_blocks * nodes_per_block``. The
worker setting then controls how many ALF tasks can run inside those allocated
resources.
.. list-table::
:header-rows: 1
:widths: 30 70
* - Setting
- What to check
* - ``partition`` / ``account``
- Queue, partition, project, or allocation name required by the scheduler.
* - ``walltime``
- Maximum runtime for the scheduler allocation.
* - ``nodes_per_block``
- Number of nodes requested for one Parsl block.
* - ``max_blocks``
- Maximum number of scheduler allocations Parsl may request for that
executor.
* - ``max_workers_per_node`` or ``max_workers``
- How many ALF tasks may execute on the allocated resources.
* - ``scheduler_options``
- Extra ``#SBATCH`` lines, such as node counts, task counts, GPUs, QoS, or
account overrides.
* - ``worker_init``
- Commands run before Parsl workers start, such as ``module load`` or
environment activation.
* - ``launcher``
- How Parsl starts workers inside an allocation. Common choices in ALF
examples include ``SimpleLauncher`` and ``SingleNodeLauncher``.
* - ``address``
- Network interface or hostname used when compute nodes must connect back
to the submit process.
The existing files in ``alframework/parsl_resource_configs`` and
``examples/*/parsl_configs.py`` show the expected structure, but cluster
partition names, accounts, modules, walltimes, and launcher choices must be
adapted for the resources you plan to use.