Synchronizing multiple tasks¶
This guide shows how multiple tasks running across several nodes communicate: how a batch script passes connection information to the tasks through environment variables, and how each task uses its rank to contribute to a shared result. As a pretext, it uses a small applied example: four tasks, spread across two nodes, computing a single sum with PyTorch or JAX.
Before you begin¶
-
Ask for a resource allocation and launch tasks on the cluster through an interactive job.
-
Track jobs through the queue, inspect and cancel them, and read their output.
What this guide covers¶
- Launching multiple tasks across two nodes with
sbatch - Passing connection information to tasks through environment variables
- Identifying each task with its rank
- Combining values from all tasks into a single result and reading it from the job output
Concept of this example¶
This example runs four tasks, two on each of two nodes. Each task holds one number equal to its rank (0, 1, 2 and 3). The tasks add their numbers together, and only the first task prints the total (6). Reaching a single total requires the tasks to communicate, which is what this example demonstrates.
The job runs on two nodes and each task is identified by its rank. The
first task (Rank 0) hosts the coordination endpoint MASTER_ADDR:MASTER_PORT
that the other tasks connect to:
graph TD
J["Job — 2 nodes × 2 tasks"] --> N0["Node 0"]
J --> N1["Node 1"]
N0 --> R0["Rank 0<br>hosts MASTER_ADDR:MASTER_PORT"]
N0 --> R1["Rank 1"]
N1 --> R2["Rank 2"]
N1 --> R3["Rank 3"]
This example launches a job (using job_***.sh) that runs one or more tasks
(whose instructions are stored in main_jax.py or main_torch.py) using
libraries (defined in pyproject.toml).
Each example is based on three files:
| File | Description |
|---|---|
job_***.sh |
Bash script used to request an allocation and launch a job (which itself runs multiple tasks based on the requested --nodes and --ntasks-per-node) |
main_***.py |
Python script containing the instructions the tasks execute. This example uses either JAX (with the script main_jax.py) or PyTorch (with the script main_torch.py) |
pyproject.toml |
Configuration file used to handle the libraries uv fetches. A separate pyproject.toml is used for each example: one for PyTorch, one for JAX |
Introducing the different files¶
In-depth script explanation on job_***.sh
Headers for the resources allocation
The #SBATCH header lines request the resource allocation: 2 nodes with 2
tasks each (4 tasks in total), 1 CPU per task, 8G of memory and a 1-minute
time limit. --ntasks-per-node fixes the number of tasks on each node, the
safe form for distributed jobs (see
Understand Slurm).
Environment variables
The environment variables MASTER_ADDR, MASTER_PORT and WORLD_SIZE are
defined here and can be retrieved in each task. MASTER_PORT derives a
per-job port from the last 4 digits of the job ID (a value in the 10000 to
19999 range), so that jobs running at the same time do not collide on the
same port. $SLURM_NTASKS holds the total number of tasks (nodes × tasks
per node), so WORLD_SIZE=$SLURM_NTASKS counts all 4 tasks. In Python,
retrieve an environment variable value as follows:
Running the tasks
srun uv run python main_***.py
- The command
sruncreates tasks. The number of tasks is determined by the allocation — here, 2 nodes × 2 tasks per node, so the command runs 4 tasks in parallel. These tasks run the command followingsrun, so each task runsuv run python main_torch.pyoruv run python main_jax.py. uv runsets up the environment for the tasks. For more information, read theuvguide on portability. It is followed by the name of the script to run in this environment.
In-depth script explanation on main_***.py
PyTorch and JAX
This guide is based on two open source examples
Environment variables
Each file retrieves the Slurm environment variables SLURM_PROCID,
SLURM_NTASKS and SLURM_NODEID. Unlike the environment variables defined
previously (MASTER_ADDR, MASTER_PORT and WORLD_SIZE), these
environment variables are specific to each task. More common Slurm
environment variables are listed in the technical
reference.
- Initialize: in PyTorch, a group is defined
-
Create a value, different for each task
The created value is based on the RANK, which is specific to each task
-
Compute their sum
- Initialize: connect the tasks together, then build a device mesh
with one named axis
ispanning all the tasks -
Create a value, different for each task
The created value is based on the RANK, which is specific to each task.
jax.make_array_from_process_local_datathen assembles the per-task values into one global array sharded along the axisi. -
Compute their sum with
jax.lax.psuminsidejax.shard_mapsee the shard_map guide
The final sum is printed from the first task of the first node (NODE_INDEX=0
and RANK=0). This is the task where all the x values have been collected.
On the other tasks, total holds a partial result.
In-depth explanation on pyproject.toml
pyproject.toml is a configuration file used by packaging tools (uv in
this case) (More info on pyproject.toml
files).
Each example has its own pyproject.toml: the PyTorch version declares
torch as a dependency, and the JAX version declares jax.
Launching the example¶
-
Create the three files on the cluster
Open the project on a compute node with
mila code, or pickmila-cpuin the Remote-SSH dropdown, then createjob_***.sh,main_***.pyandpyproject.tomlin the VSCode explorer. See VSCode and the Get Started guide.Connect to the cluster with
ssh mila, then create the files in$SCRATCHwith an editor such asvim. -
Launch the job
In the VSCode integrated terminal (or a login-node terminal), submit the job:
-
(Optional) Check the job status
See Monitor and manage jobs for how to read the output, inspect the job once it finishes, and cancel it if needed.
-
Retrieve the results
Once the job has run, its output is available in the file
slurm-<JOB_ID>.outby default, where<JOB_ID>is the ID of the job.For each example, the ranks of the tasks (that is, their
xvalues) are respectively 0, 1, 2 and 3. Their sum is collected on [Node 0 | Rank 0], which printssum=6.0:graph LR R0["Node 0 | Rank 0<br>x=0.0"] --> S["SUM"] R1["Node 0 | Rank 1<br>x=1.0"] --> S R2["Node 1 | Rank 2<br>x=2.0"] --> S R3["Node 1 | Rank 3<br>x=3.0"] --> S S --> P["Node 0 | Rank 0<br>prints sum=6.0"]
Key concepts¶
- Rank
- The unique index of a task within the job, from 0 to the number of tasks
minus 1. Read from
$SLURM_PROCIDin this example. - World size
- The total number of tasks taking part in the communication, read from
$SLURM_NTASKS. "World size" is the standard term for this value in distributed-computing frameworks such as PyTorch and JAX;$SLURM_NTASKSis simply the Slurm variable that holds it. MASTER_ADDR/MASTER_PORT- The hostname of the job's first node and a per-job port, exported by the batch script so that every task connects to the same coordination endpoint.
Next step¶
-
Launch many jobs from the same shell script
Good practice to run the same experiment with different arguments.