diff --git a/lectures/numba.md b/lectures/numba.md index 83d92a9b..477e2af5 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -687,19 +687,20 @@ effort to compute the constant $\pi$ by Monte Carlo. Now try adding parallelization and see if you get further speed gains. -You should not expect huge gains here because, while there are many -independent tasks (draw point and test if in circle), each one has low -execution time. +There are many independent tasks here (draw a point and test whether it falls +in the circle), but each one has very low execution time. -Generally speaking, parallelization is less effective when the individual -tasks to be parallelized are very small relative to total execution time. +Generally speaking, parallelization is less effective when the individual tasks +are very small relative to the overheads of spreading them across multiple CPUs. -This is due to overheads associated with spreading all of these small tasks across multiple CPUs. +The way around this is to give each thread enough work to make those overheads +worth paying. -Nevertheless, with suitable hardware, it is possible to get nontrivial speed gains in this exercise. +So, for the size of the Monte Carlo simulation, use something substantial, such +as `n = 100_000_000`. -For the size of the Monte Carlo simulation, use something substantial, such as -`n = 100_000_000`. +At that scale, and with suitable hardware, you should see a clear gain over the +serial version. ``` ```{solution-start} numba_ex3 @@ -723,36 +724,65 @@ def calculate_pi_parallel(u_draws, v_draws): return area_estimate * 4 # dividing by radius**2 ``` -Now let's see how fast it runs: +Parallelization pays off when each thread has enough work to overcome the overhead costs while +breaking the problem into parts for work to happen simultaneously. + +Let's draw a fresh, much larger set of points rather than reusing the arrays from above. + +```{note} +The two arrays below occupy about 1.6 GB of memory — reduce `n` if your +machine is short on RAM. +``` + +```{code-cell} ipython3 +n = 100_000_000 +rng = np.random.default_rng() +u_big = rng.uniform(size=n) +v_big = rng.uniform(size=n) +``` + +Now let's see how fast it runs (the second call measures runtime without +compilation time): ```{code-cell} ipython3 with qe.Timer(): - calculate_pi_parallel(u_draws, v_draws) + calculate_pi_parallel(u_big, v_big) ``` ```{code-cell} ipython3 with qe.Timer(): - calculate_pi_parallel(u_draws, v_draws) + calculate_pi_parallel(u_big, v_big) ``` -By switching parallelization on and off (selecting `True` or -`False` in the `@jit` annotation), we can test the speed gain that -multithreading provides on top of JIT compilation. +For comparison, here is the serial jitted version from {ref}`speed_ex1` on the +same points: + +```{code-cell} ipython3 +with qe.Timer(): + calculate_pi(u_big, v_big) +``` -On our workstation, we find that parallelization provides a modest but -worthwhile speed gain here. +Comparing the last two timings, multithreading provides a substantial speed +gain on top of JIT compilation. (If you are executing locally, you will get different results, depending mainly -on the number of CPUs on your machine.) +on the number of CPUs on your machine — and at small sample sizes the +parallel version can even be slower, because the gains cannot cover the cost of +distributing work across threads.) + +These two arrays are large and we are finished with them, so we release the +memory before moving on. + +```{code-cell} ipython3 +del u_big, v_big +``` Notice that we drew all of the random points *before* the loop and passed them in as arrays, so the parallel loop only *reads* from memory. Drawing the points *inside* the parallel loop instead is surprisingly delicate. - -We investigate why, and how to do it safely, in -{ref}`numba_ex_race`. +We investigate why, and how to do it safely, in {ref}`numba_ex_race`. ```{solution-end} ```