Skip to content

Commit 43b9eb9

Browse files
Merge pull request #185 from coding-for-reproducible-research/update_parallel_computing_course
Integrate PR from old parallel computing course into new material
2 parents 86e7967 + 7a8ecc3 commit 43b9eb9

File tree

6 files changed

+156
-10
lines changed

6 files changed

+156
-10
lines changed

individual_modules/parallel_computing/collective_comms.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"\n",
115115
"## Global MPI operations\n",
116116
"\n",
117-
"For distributed memory problems, its difficult to get a holistic view of your entire data set as it doesnt exist in any one place. This means that performing global operations such as calculating the sum or product of a distributed data set also requires MPI. Fortunately, MPI has several functions that make this easier. Lets create a large set of data and scatter it across our processes, as before:\n",
117+
"For distributed memory problems, it's difficult to get a holistic view of your entire data set as it doesn't exist in any one place. This means that performing global operations such as calculating the sum or product of a distributed data set also requires MPI. Fortunately, MPI has several functions that make this easier. Lets create a large set of data and scatter it across our processes, as before:\n",
118118
"\n",
119119
"```python\n",
120120
"if comm.Get_rank() == 0:\n",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Run with command:
2+
# $ python multiprocessing_fractal.py
3+
import numpy as np
4+
import warnings
5+
import matplotlib.pyplot as plt
6+
from functools import partial
7+
from multiprocessing import Pool
8+
9+
def complex_grid(extent, n_cells):
10+
mesh_range = np.arange(-extent, extent, extent/n_cells)
11+
x, y = np.meshgrid(mesh_range * 1j, mesh_range)
12+
z = x + y
13+
14+
return z
15+
16+
17+
def julia_set(grid, num_iter, c):
18+
19+
fractal = np.zeros(np.shape(grid))
20+
21+
# Iterate through the operation z := z**2 + c.
22+
for j in range(num_iter):
23+
# Catch the warnings because they are annoying
24+
with warnings.catch_warnings():
25+
warnings.simplefilter("ignore")
26+
grid = grid ** 2 + c
27+
index = np.abs(grid) < np.inf
28+
fractal[index] = fractal[index] + 1
29+
30+
return fractal
31+
32+
c = -0.83 - 0.22 * 1j
33+
extent = 2
34+
cells = 2000
35+
36+
grid = complex_grid(extent, cells)
37+
38+
# Parameters for multiprocessing
39+
n_processes = 5
40+
n_slices = 2000
41+
42+
# Split up the grid to distribute to processes
43+
sliced_grid = np.array_split(grid, n_slices)
44+
with Pool(processes=n_processes) as p:
45+
fractals = p.map(partial(julia_set, num_iter=80, c=c) , sliced_grid)
46+
47+
fractal = np.concatenate(fractals)
48+
49+
#plt.imshow(fractal, extent=[-extent, extent, -extent, extent], aspect='equal')
50+
#plt.show()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "0d031ccb-54d9-44a8-8282-63064ec52ba0",
6+
"metadata": {},
7+
"source": [
8+
"# Python Multiprocessing\n",
9+
"\n",
10+
"## Learning Objectives\n",
11+
"\n",
12+
"By the end of this lesson, learners will be able to:\n",
13+
"\n",
14+
"- Differentiate between message-passing and multiprocessing approaches in parallel programming.\n",
15+
"- Implement Python's `multiprocessing` library to parallelize a fractal generation task within a single code instance.\n",
16+
"- Set up a pool of worker processes using `Pool(processes=n_processes)` and delegate tasks across these processes with the `p.map()` function.\n",
17+
"- Use `functools.partial` to manage function parameters that remain constant across parallel tasks, optimizing code reuse.\n",
18+
"- Divide a computational grid into slices and assign each slice to a worker process to handle independently.\n",
19+
"- Close a pool of processes in Python's multiprocessing model once tasks are completed, resuming the main program.\n",
20+
"- Evaluate the performance of the multiprocessing approach by timing code execution with varying numbers of slices and processes, and compare results with the serial version in `fractal_complete.py`.\n",
21+
"\n",
22+
"\n",
23+
"## Fractal example with Python multiprocessing\n",
24+
"\n",
25+
"In the previous lessons we have seen *message passing* being used to communicate data between multiple running instances of the code.\n",
26+
"An alternative approach is to use *multi-processing*, where-by we launch one instance of our code which in turn launches new threads with access to the same memory.\n",
27+
"\n",
28+
"In `multiprocessing_fractal.py`, the previous fractal example has been implemented using `multiprocessing` from the python standard library.\n",
29+
"Most of the code follows the same structure as the parallel fractal example.\n",
30+
"\n",
31+
"For the multi-processing model, we set up a *pool* of workers, `Pool(processes=n_processes)`, assigned to `p`.\n",
32+
"The work can then be delegated out to these workers using the [`p.map()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.map) method.\n",
33+
"This `map` method (equivalent to the builtin [`map`](https://docs.python.org/3/library/functions.html#map)) takes two arguments: a function to run (our fractal function), and a collection of inputs to pass to the function (different regions of the grid to be processed in parallel).\n",
34+
"\n",
35+
"```{note}\n",
36+
"To pass in the parameters that don't change over grid regions, we've used [`functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial):\n",
37+
"\n",
38+
"``` python\n",
39+
"partial_julia_set = partial(julia_set, num_iter=80, c=-0.83 - 0.22 * 1j)\n",
40+
"```\n",
41+
"\n",
42+
"This would be essentially equivalent to defining a new function:\n",
43+
"\n",
44+
"``` python\n",
45+
"def partial_julia_set(grid):\n",
46+
" return julia_set(grid, num_iter=80, c=-0.83 -0.22 * 1j)\n",
47+
"```\n",
48+
"\n",
49+
"You may be familiar with *lambda* expressions, but these cannot be passed in to the `multiprocessing.Pool.map` function.\n",
50+
"In this script, we have split up the grid into `n_slices` vertical slices and assigned a pool of of `n_processes` workers.\n",
51+
"These workers each take a slice, calculate the result saving the output into `fractals`, then work on a new slice.\n",
52+
"When there are no more slices to work on, the pool is *closed* and the program resumes.\n",
53+
"We can see how we can speed up the code by timing the full script running with different values of `n_slices` and `n_processes`.\n",
54+
"Compare these numbers against the previous serial example in `fractal_complete.py`."
55+
]
56+
},
57+
{
58+
"cell_type": "markdown",
59+
"id": "b45c5b07-4d05-4705-93fe-fd841171e4cc",
60+
"metadata": {},
61+
"source": [
62+
"# Complete File\n",
63+
"[Download complete multiprocessing_fractal.py file](complete_files/multiprocessing_fractal.py)"
64+
]
65+
},
66+
{
67+
"cell_type": "code",
68+
"execution_count": null,
69+
"id": "b3b04584-c002-47f5-8f1c-66cb00ebe4d3",
70+
"metadata": {},
71+
"outputs": [],
72+
"source": []
73+
}
74+
],
75+
"metadata": {
76+
"kernelspec": {
77+
"display_name": "Python 3 (ipykernel)",
78+
"language": "python",
79+
"name": "python3"
80+
},
81+
"language_info": {
82+
"codemirror_mode": {
83+
"name": "ipython",
84+
"version": 3
85+
},
86+
"file_extension": ".py",
87+
"mimetype": "text/x-python",
88+
"name": "python",
89+
"nbconvert_exporter": "python",
90+
"pygments_lexer": "ipython3",
91+
"version": "3.9.19"
92+
}
93+
},
94+
"nbformat": 4,
95+
"nbformat_minor": 5
96+
}

individual_modules/parallel_computing/parallel_fractal.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"\n",
2121
"## Solving a problem in parallel\n",
2222
"\n",
23-
"In the previous three sections we have built up a foundation enough to be able to tackle a simple problem in parallel. In this case, the problem we will attempt to solve is constructing a fractal. This kind of problem is often known as \"embarassingly parallel\" meaning that each element of the result has no dependency on any of the other elements, meaning that we can solve this problem in parallel without too much difficulty. Let's get started by creating a new script - `parallel_fractal.py```:\n",
23+
"In the previous three sections we have built up a foundation enough to be able to tackle a simple problem in parallel. In this case, the problem we will attempt to solve is constructing a fractal. This kind of problem is often known as \"embarrassingly parallel\" meaning that each element of the result has no dependency on any of the other elements, meaning that we can solve this problem in parallel without too much difficulty. Let's get started by creating a new script - `parallel_fractal.py`:\n",
2424
"\n",
2525
"## Setting up our problem\n",
2626
"\n",
@@ -61,7 +61,7 @@
6161
" return fractal\n",
6262
"```\n",
6363
"\n",
64-
"This function calculates how many iterations it takes for each element in the complex grid to reach infinity (if ever) when operated on with the equation `x = x**2 + c```. The function itself is not the focus of this exercise as much as it is a way to make the computer perform some work! Let's use these functions to set up our problem in serial, without any parallelism:\n",
64+
"This function calculates how many iterations it takes for each element in the complex grid to reach infinity (if ever) when operated on with the equation `x = x**2 + c`. The function itself is not the focus of this exercise as much as it is a way to make the computer perform some work! Let's use these functions to set up our problem in serial, without any parallelism:\n",
6565
"\n",
6666
"```python\n",
6767
"\n",
@@ -75,7 +75,7 @@
7575
"fractal = julia_set(grid, 80, c)\n",
7676
"```\n",
7777
"\n",
78-
"If we run the python script (```python fractal.py```) it takes a few seconds to complete (this will vary depending on your machine), so we can already see that we are making our computer work reasonably hard with just a few lines of code. If we use the `time` command we can get a simple overview of how much time and resource are being used:\n",
78+
"If we run the python script (`python fractal.py`) it takes a few seconds to complete (this will vary depending on your machine), so we can already see that we are making our computer work reasonably hard with just a few lines of code. If we use the `time` command we can get a simple overview of how much time and resource are being used:\n",
7979
"\n",
8080
"```\n",
8181
"$ time python parallel_fractal_complete.py\n",
@@ -137,7 +137,7 @@
137137
"mpirun -n 4 python parallel_fractal.py 37.23s user 21.70s system 370% cpu 15.895 total\n",
138138
"```\n",
139139
"\n",
140-
"We can see that running the problem in parallel has greatly increased the speed of the function, but that the speed increase is directly proportional to the resource we are using (i.e. using 4 cores doesnt make the process 4 times faster). This is due to the increased overhead induced by MPI communication procedures, which can be quite expensive (as metioned in previous chapters).\n",
140+
"We can see that running the problem in parallel has greatly increased the speed of the function, but that the speed increase is directly proportional to the resource we are using (i.e. using 4 cores doesn't make the process 4 times faster). This is due to the increased overhead induced by MPI communication procedures, which can be quite expensive (as mentioned in previous chapters).\n",
141141
"The way that a program performance changes based on the number of processes it runs on is often referred to as its \"scaling behaviour\". Determining how your problem scales across multiple processes is a useful exercise and is helpful when it comes to porting your code to a larger scale HPC machine.\n",
142142
"\n",
143143
"### Download Complete Parallel File \n",

individual_modules/parallel_computing/simple_communication.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,25 @@
5757
"```\n",
5858
"\n",
5959
"Now, if we run this script in parallel we no longer get the error, because the variable now exists on the second rank thanks to the `send`/`recv` methods.\n",
60-
"In order to add an additional layer of safety to this process, we can add a tag to the message. This is an integer ID which ensures that the message is being recieved is being correctly used by the recieving process. This can be simply achieved by modifying the code to match the following:\n",
60+
"In order to add an additional layer of safety to this process, we can add a tag to the message. This is an integer ID which ensures that the message is being received is being correctly used by the receiving process. This can be simply achieved by modifying the code to match the following:\n",
6161
"\n",
6262
"```python\n",
6363
" comm.send(var, dest=1, tag=23)\n",
6464
"...\n",
6565
" var = comm.recv(source=0, tag=23)\n",
6666
"```\n",
6767
"\n",
68-
"The types of communications provided by the `send```/```recv` methods are known as blocking communications, as there is a chance that the send process won't return until it gets a signal that the data has been recieved successfully. This means that sending large amounts of data between processes can result in significant stoppages to the program. In practice, the standard for this is not implemented uniformly, so the blocking/non-blocking nature of the communication can be dynamic or depend on the size of the message being passed.\n",
68+
"The types of communications provided by the `send```/```recv` methods are known as blocking communications, as there is a chance that the send process won't return until it gets a signal that the data has been received successfully. This means that sending large amounts of data between processes can result in significant stoppages to the program. In practice, the standard for this is not implemented uniformly, so the blocking/non-blocking nature of the communication can be dynamic or depend on the size of the message being passed.\n",
6969
"Before we start the next example, we can add the line `comm.barrier()` in our Python script to make sure that our processes only proceed once all other processes have reached this point, which will stop us getting confused about the output of our program.\n",
7070
"\n",
7171
"## Non-blocking communications\n",
7272
"\n",
73-
"In some instances, it might make sense for communications to only be non-blocking, which will enable the sending rank to continue with its process without needing to wait for confirmation of a potentially large message to be recieved. In this case, we can use the explicitly non-blocking methods, `isend` and `irecv`.\n",
73+
"In some instances, it might make sense for communications to only be non-blocking, which will enable the sending rank to continue with its process without needing to wait for confirmation of a potentially large message to be received. In this case, we can use the explicitly non-blocking methods, `isend` and `irecv`.\n",
7474
"The syntax is very similar for the sending process:\n",
7575
"```python\n",
7676
" comm.send(var, dest=1, tag=23)\n",
7777
"```\n",
78-
"but the recieving process has more to unpack. The `comm.irecv` method returns a request object, which can be unpacked with the `wait` method which then returns the data:\n",
78+
"but the receiving process has more to unpack. The `comm.irecv` method returns a request object, which can be unpacked with the `wait` method which then returns the data:\n",
7979
"\n",
8080
"```python\n",
8181
"if comm.Get_rank() == 0:\n",

programme_information/parallel_computing.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
"\n",
168168
"### MPI for Python (MPI4Py)\n",
169169
"\n",
170-
"The first part of this workshop is focussed on distributed memory parallelism with MPI, making use of the Python programming language. There are many different interfaces to MPI for many different languages, so we've chosen Python for the benefits it provides to write examples in an easy-to-understand format. Whilst the specific syntax of the commands learned in this part of the course wont be applicable across different languages, the overall code structures and concepts are highly transferrable, so once you have a solid grasp of the fundamentals of MPI you should be able to take thoses concepts to any language with an MPI interface and write parallel code!\n",
170+
"The first part of this workshop is focussed on distributed memory parallelism with MPI, making use of the Python programming language. There are many different interfaces to MPI for many different languages, so we've chosen Python for the benefits it provides to write examples in an easy-to-understand format. Whilst the specific syntax of the commands learned in this part of the course wont be applicable across different languages, the overall code structures and concepts are highly transferable, so once you have a solid grasp of the fundamentals of MPI you should be able to take those concepts to any language with an MPI interface and write parallel code!\n",
171171
"\n",
172172
"The python package that we will be using in this course to implement MPI command is the [MPI4Py package](https://mpi4py.readthedocs.io/en/stable/), which can be installed via pip as follows:\n",
173173
"```\n",

0 commit comments

Comments
 (0)