In order to successfully complete this assignment you must do the required reading, watch the provided videos and complete all instructions. The embedded survey form must be entirely filled out and submitted on or before 11:59pm. Students must come to class the next day prepared to discuss the material covered in this assignment.


PCA 25: Parallel Python#

Goals for today’s pre-class assignment#

  1. Matrix Multiply Example

  2. Parallel Python example

  3. The Python GIL (Global Interface Lock)

  4. Getting around the GIL

  5. Assignment wrap up


1. Matrix Multiply Example#

The following is a simple implementation of a matrix multiply written in python. Review the code try to understand what it is doing.

%matplotlib inline
import matplotlib.pylab as plt
import numpy as np
import sympy as sp
import random
import time
sp.init_printing(use_unicode=True)
#simple matrix multiply (no numpy)
def multiply(m1,m2):
    m = len(m1)
    d = len(m2)
    n = len(m2[0])
    if len(m1[0]) != d:
        print("ERROR - inner dimentions not equal")
    result = [[0 for i in range(m)] for j in range(n)]
    for i in range(0,m):
        for j in range(0,n):
            for k in range(0,d):
                result[i][j] = result[i][j] + m1[i][k] * m2[k][j]
    return result
# Random generated 2d lists of lists that can be multiplied 
m = 4
d = 10
n = 4

A = [[random.random() for i in range(d)] for j in range(m)]
B = [[random.random() for i in range(n)] for j in range(d)]
#Compute matrix multiply using your function
start = time.time()

simple_answer = multiply(A, B)
simple_time = time.time()-start

print('simple_answer =',simple_time,'seconds')

Lets compare this to the numpy result:

#Compare to numpy result
start = time.time()

np_answer = np.matrix(A)*np.matrix(B)
np_time = time.time()-start

print('np_answer =',np_time,'seconds')

For this example, numpy result are most likely slower than the simple result. Think about why this might be. We will discuss this later.

DO THIS: See if you can write a loop to do a scaling study for the above code. Loop over the value of \(n\) such that \(n\) is 4, 16, 32, 64, 128 and 256. For each iteration generate two random matrices (as above) with \(m = d = n\). Then time the matrix multiply for the provided function and again for the numpy function. Graph the results as size of \(n\) vs time.

# Put your code here
##ANSWER##
simple_time = []
np_time = []
n_vals = [4,16,32,64,128, 256]
for n in n_vals:
    
    m = n
    d = n

    A = [[random.random() for i in range(d)] for j in range(m)]
    B = [[random.random() for i in range(n)] for j in range(d)]
    
    start = time.time()

    simple_answer = multiply(A, B)
    simple_time.append(time.time()-start)

    start = time.time()

    np_answer = np.matrix(A)*np.matrix(B)
    np_time.append(time.time()-start)

plt.plot(n_vals,np_time)
plt.plot(n_vals,simple_time)
##ANSWER##

DO THIS: Explore the Internet for ways to speed up Python (There are a lot of them). Save some of your search results in the cell below and come to class prepaired to discuss what you found.

Put your search results here.


2. Parallel Python example#

Here is an example for running parallel python using the multiprocessing library. Note, that running multiprocessing in Python doesn’t generally work well from a Jupyter notebook, so you should copy the Python code into a .py file and run it directly from the command line if you encounter errors.

https://stackoverflow.com/questions/10415028/how-can-i-recover-the-return-value-of-a-function-passed-to-multiprocessing-proce

import multiprocessing
num_procs = multiprocessing.cpu_count()
print('You have', num_procs, 'processors')

def worker(procnum, return_dict):
    '''worker function'''
    print(str(procnum) + ' represent!')
    return_dict[procnum] = procnum


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    return_dict = manager.dict()
    jobs = []
    for i in range(num_procs):
        p = multiprocessing.Process(target=worker, args=(i,return_dict))
        jobs.append(p)
        p.start()

    for proc in jobs:
        proc.join()
    print(return_dict.values())

Lets try to make a parallel matrix multiply#

The following is the instructor’s attempt at using multiprocessing to do matrix multiply. First lets start with a serial method.

%matplotlib inline
import matplotlib.pylab as plt
import numpy as np
import sympy as sp
import random
import time
sp.init_printing(use_unicode=True)
#simple matrix multiply (no numpy)
def multiply(m1,m2):
    m = len(m1)
    d = len(m2)
    n = len(m2[0])
    if len(m1[0]) != d:
        print("ERROR - inner dimentions not equal")
    result = [[0 for i in range(m)] for j in range(n)]
    for i in range(0,m):
        for j in range(0,n):
            for k in range(0,d):
                result[i][j] = result[i][j] + m1[i][k] * m2[k][j]
    return result
# Random generated 2d lists of lists that can be multiplied 
m = 4
d = 10
n = 4

A = [[random.random() for i in range(d)] for j in range(m)]
B = [[random.random() for i in range(n)] for j in range(d)]
#Compute matrix multiply using your function


start = time.time()

simple_answer = multiply(A, B)
simple_time = time.time()-start

print('simple_answer =',simple_time,'seconds')

Lets compare this to the numpy result:

#Compare to numpy result
start = time.time()

np_answer = np.matrix(A)*np.matrix(B)
np_time = time.time()-start

print('np_answer =',np_time,'seconds')
#Compare to numpy result
A_ = np.matrix(A)
B_ = np.matrix(B)

start = time.time()

np_answer = A_*B_
np_time = time.time()-start

print('np_answer =',np_time,'seconds')
np.allclose(simple_answer,np_answer)

Now lets use multiprocessing to try and do a parallel method#

#Attempt at a parallel multiply
import matplotlib.pylab as plt
import numpy as np
import sympy as sp
import random
import time
import multiprocessing
num_procs = multiprocessing.cpu_count()
sp.init_printing(use_unicode=True)
m = 4
d = 10
n = 4

A = [[random.random() for i in range(d)] for j in range(m)]
B = [[random.random() for i in range(n)] for j in range(d)]

def multiply(m1,m2):
    m = len(m1)
    d = len(m2)
    n = len(m2[0])
    if len(m1[0]) != d:
        print("ERROR - inner dimentions not equal")
    result = [[0 for i in range(m)] for j in range(n)]
    for i in range(0,m):
        for j in range(0,n):
            for k in range(0,d):
                result[i][j] = result[i][j] + m1[i][k] * m2[k][j]
    return result


def compute_element(args):
    i, j, m1, m2 = args
    return i, j, sum(m1[i][k] * m2[k][j] for k in range(len(m2)))

def parallel_multiply(m1, m2):
    m, d, n = len(m1), len(m2), len(m2[0])
    
    if len(m1[0]) != d:
        raise ValueError("ERROR - inner dimensions not equal")
    
    result = [[0] * n for _ in range(m)]
    
    with multiprocessing.Pool() as pool:
        indices = [(i, j, m1, m2) for i in range(m) for j in range(n)]
        for i, j, value in pool.map(compute_element, indices):
            result[i][j] = value
    
    return result
if __name__ == "__main__":
    

    #Parallel result
    start = time.time()

    parallel_answer = parallel_multiply(A, B)
    parallel_time = time.time()-start


    #Serial Result
    start = time.time()
    serial_answer = multiply(A,B)
    serial_time = time.time()-start
    
    #Numpy result
    A_ = np.matrix(A)
    B_ = np.matrix(B)

    start = time.time()

    np_answer = A_*B_
    np_time = time.time()-start

    print('np_answer =',np_time,'seconds')
    print('parallel_answer=',parallel_time,'seconds')
    print('serial_answer=',serial_time,'seconds')


    print("\n_______\nParallel\n")
    for row in parallel_answer:
        print(row)

    print("\n________\nSerial\n")
    for row in serial_answer:
        print(row)

    print("\n________\nNumpy\n")
    for row in np_answer:
        print(row)
import numpy as np
import matplotlib.pyplot as plt
parallel_time = 
serial_time = 
np_time = 
objects = ('Simple', 'Numpy', 'parallel')
y_pos = np.arange(len(objects))
performance = [simple_time,np_time,parallel_time]
 
plt.bar(y_pos, performance, align='center', alpha=0.5)
plt.xticks(y_pos, objects)
plt.ylabel('Time (seconds)')
plt.yscale('log')
plt.title('Programming language usage')
 

QUESTION: Why do you think the parallel version was so much slower than Python?

Put your answer to the above question here.


3. The Python GIL (Global Interface Lock)#

DO THIS: Read the following blog post and answer the questions: https://wiki.python.org/moin/GlobalInterpreterLock

QUESTION: Why was the GIL introduced to the Python programming language?

Put your answer to the above question here.

QUESTION: How does the GIL help avoid race conditions?

Put your answer to the above question here.

QUESTION: How does the GIL help avoid deadlock?

Put your answer to the above question here.

QUESTION: Why is the GIL problematic to parallel libraries like the “thread” and “multiprocessing” libraries?

Put your answer to the above question here.


4. Getting around the GIL#

Fortunately there are ways to get around the GIL. In fact, Python has libraries that do shared memory parallelization, shared network parallelization and GPU acceleration. Do some research and answer the following questions:

QUESTION: Some of numpy library can run in parallel. How does numpy get around the GIL?

Put your answer to the above question here.

QUESTION: The numba library can also run in parallel. How does numba get around the GIL?

Put your answer to the above question here.

QUESTION: What python library can be used to program GPUs?

Put your answer to the above question here.

QUESTION: What python library can be used to run shared network parallelization such as the Message Passing Interface (MPI)?

Put your answer to the above question here.

QUESTION: There seem to be a lot of solutions for running Python in parallel. Provide an argument(s) as to why you would bother with an “older” language such as C/C++ or Fortran?

Put your answer to the above question here.


5. Assignment wrap-up#

Please fill out the form that appears when you run the code below. You must completely fill this out in order to receive credits for the assignment!

Direct Link to Survey Form

Put your answer to the above question here

QUESTION: Summarize what you did in this assignment.

Put your answer to the above question here

QUESTION: What questions do you have, if any, about any of the topics discussed in this assignment after working through the jupyter notebook?

Put your answer to the above question here

QUESTION: How well do you feel this assignment helped you to achieve a better understanding of the above mentioned topic(s)?

Put your answer to the above question here

QUESTION: What was the most challenging part of this assignment for you?

Put your answer to the above question here

QUESTION: What was the least challenging part of this assignment for you?

Put your answer to the above question here

QUESTION: What kind of additional questions or support, if any, do you feel you need to have a better understanding of the content in this assignment?

Put your answer to the above question here

QUESTION: Do you have any further questions or comments about this material, or anything else that’s going on in class?

Put your answer to the above question here

QUESTION: Approximately how long did this pre-class assignment take?

Put your answer to the above question here

from IPython.display import HTML
HTML(
"""
<iframe 
	src="https://cmse.msu.edu/cmse401-pc-survey" 
	width="100%" 
	height="500px" 
	frameborder="0" 
	marginheight="0" 
	marginwidth="0">
	Loading...
</iframe>
"""
)

Congratulations, we’re done!#

To get credit for this assignment you must fill out and submit the above survey from on or before the assignment due date.

Written by Dr. Dirk Colbry, Michigan State University (Updated by Dr. Nathan Haut in Spring 2025) Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.