-
-
Notifications
You must be signed in to change notification settings - Fork 30
[numba.md] Update np.random → Generator API #550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
397a891
aef82bd
f8f02b7
ff43ce6
f2c6503
284cd96
5073452
0cf0d7d
1359bb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -454,11 +454,17 @@ Compare speed with and without Numba when the sample size is large. | |
| Here is one solution: | ||
|
|
||
| ```{code-cell} ipython3 | ||
| n = 1_000_000 | ||
| rng = np.random.default_rng() | ||
| u_draws = rng.uniform(size=n) | ||
| v_draws = rng.uniform(size=n) | ||
|
|
||
| @jit | ||
| def calculate_pi(n=1_000_000): | ||
| def calculate_pi(u_draws, v_draws): | ||
| n = len(u_draws) | ||
| count = 0 | ||
| for i in range(n): | ||
| u, v = np.random.uniform(0, 1), np.random.uniform(0, 1) | ||
| u, v = u_draws[i], v_draws[i] | ||
| d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2) | ||
| if d < 0.5: | ||
| count += 1 | ||
|
|
@@ -471,12 +477,12 @@ Now let's see how fast it runs: | |
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| calculate_pi() | ||
| calculate_pi(u_draws, v_draws) | ||
| ``` | ||
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| calculate_pi() | ||
| calculate_pi(u_draws, v_draws) | ||
| ``` | ||
|
|
||
| If we switch off JIT compilation by removing `@jit`, the code takes around | ||
|
|
@@ -550,10 +556,13 @@ p, q = 0.1, 0.2 # Prob of leaving low and high state respectively | |
| Here's a pure Python version of the function | ||
|
|
||
| ```{code-cell} ipython3 | ||
| def compute_series(n): | ||
| n = 1_000_000 | ||
| rng = np.random.default_rng() | ||
| U = rng.uniform(0, 1, size=n) | ||
|
|
||
| def compute_series(n, U): | ||
| x = np.empty(n, dtype=np.int64) | ||
| x[0] = 1 # Start in state 1 | ||
| U = np.random.uniform(0, 1, size=n) | ||
| for t in range(1, n): | ||
| current_x = x[t-1] | ||
| if current_x == 0: | ||
|
HumphreyYang marked this conversation as resolved.
|
||
|
|
@@ -567,8 +576,7 @@ Let's run this code and check that the fraction of time spent in the low | |
| state is about 0.666 | ||
|
|
||
| ```{code-cell} ipython3 | ||
| n = 1_000_000 | ||
| x = compute_series(n) | ||
| x = compute_series(n, U) | ||
| print(np.mean(x == 0)) # Fraction of time x is in state 0 | ||
| ``` | ||
|
|
||
|
|
@@ -578,7 +586,7 @@ Now let's time it: | |
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| compute_series(n) | ||
| compute_series(n, U) | ||
| ``` | ||
|
|
||
| Next let's implement a Numba version, which is easy | ||
|
|
@@ -590,15 +598,15 @@ compute_series_numba = jit(compute_series) | |
| Let's check we still get the right numbers | ||
|
|
||
| ```{code-cell} ipython3 | ||
| x = compute_series_numba(n) | ||
| x = compute_series_numba(n, U) | ||
| print(np.mean(x == 0)) | ||
| ``` | ||
|
|
||
| Let's see the time | ||
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| compute_series_numba(n) | ||
| compute_series_numba(n, U) | ||
| ``` | ||
|
|
||
| This is a nice speed improvement for one line of code! | ||
|
|
@@ -636,11 +644,17 @@ For the size of the Monte Carlo simulation, use something substantial, such as | |
| Here is one solution: | ||
|
|
||
| ```{code-cell} ipython3 | ||
| n = 1_000_000 | ||
| rng = np.random.default_rng() | ||
| u_draws = rng.uniform(size=n) | ||
| v_draws = rng.uniform(size=n) | ||
|
HumphreyYang marked this conversation as resolved.
Outdated
|
||
|
|
||
| @jit(parallel=True) | ||
| def calculate_pi(n=1_000_000): | ||
| def calculate_pi(u_draws, v_draws): | ||
| n = len(u_draws) | ||
| count = 0 | ||
| for i in prange(n): | ||
| u, v = np.random.uniform(0, 1), np.random.uniform(0, 1) | ||
| u, v = u_draws[i], v_draws[i] | ||
| d = np.sqrt((u - 0.5)**2 + (v - 0.5)**2) | ||
| if d < 0.5: | ||
| count += 1 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this avoids the thread-safety issue, but the timing comparison is no longer quite apples-to-apples because the random draws have been moved outside the timed function. Personally, I think we can generate the draws in the first solution by moving n = 1_000_000
rng = np.random.default_rng()
u_draws = rng.uniform(size=n)
v_draws = rng.uniform(size=n)to line 457, and modifying the function in the code block starting at line 457 (solution to exercise 1) to take In this case, we would have a fair timing comparison. Please let me know what you think!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @mmcky, I think this is an example where we need to make a tough choice between keeping the legacy API for simplicity and using the new
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @HumphreyYang I agree with you. I have added some additional notes below regarding double checking |
||
|
|
@@ -653,12 +667,12 @@ Now let's see how fast it runs: | |
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| calculate_pi() | ||
| calculate_pi(u_draws, v_draws) | ||
| ``` | ||
|
|
||
| ```{code-cell} ipython3 | ||
| with qe.Timer(): | ||
| calculate_pi() | ||
| calculate_pi(u_draws, v_draws) | ||
| ``` | ||
|
|
||
| By switching parallelization on and off (selecting `True` or | ||
|
|
@@ -750,6 +764,9 @@ $$ | |
|
|
||
| Using this fact, the solution can be written as follows. | ||
|
|
||
| Note that random draws are kept inside the inner loop rather than pre-allocated, | ||
| to avoid creating large shock arrays of size `M * n`. | ||
|
|
||
|
HumphreyYang marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```{code-cell} ipython3 | ||
| M = 10_000_000 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded timing number is likely stale now. The old
calculate_pidrew randoms inside the loop (np.random.uniform(...)calls); the new version just indexes into pre-drawn arrays. Removing the RNG calls from the loop body changes the no-JIT Python baseline substantially, so the "~150×" ratio this sentence quotes — and the "2 orders of magnitude" claim two lines down — were measured against the old code and are probably no longer accurate.More broadly, hard-coding exact multipliers into the lecture is fragile: they drift silently on every NumPy/Numba/hardware bump. Recommend re-measuring under the new code and softening to qualitative language, e.g. "more than two orders of magnitude faster on our machine" / "dramatically faster", rather than a precise figure.