From 57107b2f325f72cebdfab1e0899d273ae8b2bbc2 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Tue, 4 Aug 2026 10:44:12 +1000 Subject: [PATCH 1/4] [numba.md] numba_ex3: run the solution at the n the exercise asks for The statement says to use a substantial sample size such as n = 100_000_000, but the solution reused the shared 10^6 arrays --- at that size both timed cells display as 0.00 seconds, demonstrating nothing. The solution now draws its own 10^8 points (with a memory note), times the parallel version, and compares against speed_ex1's serial jitted function on the same arrays so the multithreading gain is visible on the page. The shared 10^6 arrays are unchanged: speed_ex1's pure-Python comparison would take minutes at 10^8. Co-Authored-By: Claude Fable 5 --- lectures/numba.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index 83d92a9b..4366d6ba 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -723,27 +723,50 @@ def calculate_pi_parallel(u_draws, v_draws): return area_estimate * 4 # dividing by radius**2 ``` -Now let's see how fast it runs: +As the exercise statement suggests, parallelization pays off when each thread +has a substantial amount of work to do, so we 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 — around 3x on our workstation. (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.) 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. From b9b9c87ca46bdf24f55b2b6411e859cacbffbf01 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 12 Aug 2026 12:08:58 +1000 Subject: [PATCH 2/4] @mmcky edit of lecture wording --- lectures/numba.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index 4366d6ba..d155f018 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -723,9 +723,10 @@ def calculate_pi_parallel(u_draws, v_draws): return area_estimate * 4 # dividing by radius**2 ``` -As the exercise statement suggests, parallelization pays off when each thread -has a substantial amount of work to do, so we draw a fresh, much larger set of -points rather than reusing the arrays from above. +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. + +Lets 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 @@ -773,9 +774,7 @@ 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} ``` From 6246b07a7676167f6c8eb0e8028afbfe57c4ad08 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 12 Aug 2026 12:12:20 +1000 Subject: [PATCH 3/4] [numba.md] numba_ex3: release the 1e8 arrays after the timings The `u_big`/`v_big` pair added for numba_ex3 stayed alive for the rest of the notebook. Because `numba_ex_draw_speed` rebinds `u_draws`/`v_draws` to a second pair of 1e8-element arrays, peak memory reached ~3.2 GB rather than ~1.6 GB. That is comfortable on the g4dn.2xlarge CI runner but not on the smaller machines the accompanying memory note is written for. Delete the arrays after their last use, and fix "Lets" -> "Let's" plus the trailing whitespace introduced alongside it. Co-Authored-By: Claude Fable 5 --- lectures/numba.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index d155f018..a2ccf8d1 100644 --- a/lectures/numba.md +++ b/lectures/numba.md @@ -723,10 +723,10 @@ def calculate_pi_parallel(u_draws, v_draws): return area_estimate * 4 # dividing by radius**2 ``` -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. +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. -Lets draw a fresh, much larger set of points rather than reusing the arrays from above. +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 @@ -769,6 +769,13 @@ 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. From 2e746344e80bf4ce9b456085a4b01f2be2cc2878 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 12 Aug 2026 12:31:02 +1000 Subject: [PATCH 4/4] [numba.md] numba_ex3: align the prose with the measured timings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Netlify preview of 6246b07 reports 0.1448s serial against 0.0353s parallel at n=100_000_000, a 4.1x gain, but the solution claimed "around 3x on our workstation". Drop the multiple rather than restate it: these lectures re-execute every build on a shared runner, so a pinned figure drifts, and line 765 was the only hardcoded speedup in the file — the rest describes gains qualitatively and lets the printed timings carry the fact. The exercise preamble still warned "you should not expect huge gains here", which was written for the old small-n setup and now contradicts the near-linear scaling the solution demonstrates. Reframe it around giving each thread enough work, which is why the exercise asks for a large n. Prose only, so the execution cache for the code cells is unaffected. Co-Authored-By: Claude Fable 5 --- lectures/numba.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lectures/numba.md b/lectures/numba.md index a2ccf8d1..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 @@ -762,7 +763,7 @@ with qe.Timer(): ``` Comparing the last two timings, multithreading provides a substantial speed -gain on top of JIT compilation — around 3x on our workstation. +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 — and at small sample sizes the