@@ -42,37 +42,39 @@ import matplotlib.pyplot as plt
4242```
4343444445-4645## Overview
47464847In an {doc}`earlier lecture <need_for_speed>` we discussed vectorization,
4948which can improve execution speed by sending array processing operations in batch to efficient low-level code.
50495150However, as {ref}`discussed in that lecture <numba-p_c_vectorization>`,
52-traditional vectorization schemes, such as those found in MATLAB and NumPy, have weaknesses.
51+traditional vectorization schemes have weaknesses:
53525453* Highly memory-intensive for compound array operations
55-* Ineffective or impossible for some algorithms.
54+* Ineffective or impossible for some algorithms
56555756One way to circumvent these problems is by using [Numba](https://numba.pydata.org/), a
58-**just in time (JIT) compiler** for Python that is oriented towards numerical work.
57+**just in time (JIT) compiler** for Python.
595860-Numba compiles functions to native machine code instructions during runtime.
59+Numba compiles functions to native machine code instructions at runtime.
61606261When it succeeds, the result is performance comparable to compiled C or Fortran.
636264-In addition, Numba can do other useful tricks, such as {ref}`multithreading` or
65-interfacing with GPUs (through `numba.cuda`).
63+In addition, Numba can do useful tricks such as {ref}`multithreading <multithreading>`.
666467-Numba's JIT compiler is in many ways similar to the JIT compiler in Julia
65+This lecture introduces the core ideas.
686669-The main difference is that it is less ambitious, attempting to compile a smaller subset of the language.
706771-Although this might sound like a deficiency, it is in some ways an advantage.
68+```{note}
69+Some readers might be curious about the relationship between Numba and [Julia](https://julialang.org/),
70+which contains its own JIT compiler. While the two compilers are similar in
71+many ways, Numba is less ambitious, attempting only to compile a small subset of
72+the Python language. Although this might sound like a deficiency, it is also a
73+strength: the more restrictive nature of Numba makes it easy to use well and
74+good at what it does.
75+```
727673-Numba is lean, easy to use, and very good at what it does.
747775-This lecture introduces the core ideas.
76787779(numba_link)=
7880## {index}`Compiling Functions <single: Compiling Functions>`
@@ -93,16 +95,14 @@ $$
9395 x_{t+1} = \alpha x_t (1 - x_t)
9496$$
959796-In what follows we set
98+In what follows we set $\alpha = 4$.
979998-```{code-cell} ipython3
99-α = 4.0
100-```
100+#### Base Version
101101102102Here's the plot of a typical trajectory, starting from $x_0 = 0.1$, with $t$ on the x-axis
103103104104```{code-cell} ipython3
105-def qm(x0, n):
105+def qm(x0, n, α=4.0):
106106 x = np.empty(n+1)
107107 x[0] = x0
108108 for t in range(n):
@@ -117,103 +117,119 @@ ax.set_ylabel('$x_{t}$', fontsize = 12)
117117plt.show()
118118```
119119120-To speed the function `qm` up using Numba, our first step is
120+Let's see how long this takes to run for large $n$
121121122122```{code-cell} ipython3
123-from numba import jit
123+n = 10_000_000
124+125+with qe.Timer() as timer1:
126+ # Time Python base version
127+ x = qm(0.1, int(n))
124128125-qm_numba = jit(qm)
126129```
127130128-The function `qm_numba` is a version of `qm` that is "targeted" for
129-JIT-compilation.
130131131-We will explain what this means momentarily.
132+#### Acceleration via Numba
133+134+To speed the function `qm` up using Numba, we first import the `jit` function
132135133-Let's time and compare identical function calls across these two versions, starting with the original function `qm`:
134136135137```{code-cell} ipython3
136-n = 10_000_000
138+from numba import jit
139+```
137140138-with qe.Timer() as timer1:
139- qm(0.1, int(n))
140-time1 = timer1.elapsed
141+Now we apply it to `qm`, producing a new function:
142+143+```{code-cell} ipython3
144+qm_numba = jit(qm)
141145```
142146143-Now let's try qm_numba
147+The function `qm_numba` is a version of `qm` that is "targeted" for
148+JIT-compilation.
149+150+We will explain what this means momentarily.
151+152+Let's time this new version:
144153145154```{code-cell} ipython3
146155with qe.Timer() as timer2:
147- qm_numba(0.1, int(n))
148-time2 = timer2.elapsed
156+ # Time jitted version
157+ x = qm_numba(0.1, int(n))
149158```
150159151-This is already a very large speed gain.
160+This is a large speed gain.
152161153-In fact, the next time and all subsequent times it runs even faster as the function has been compiled and is in memory:
162+In fact, the next time and all subsequent times it runs even faster as the
163+function has been compiled and is in memory:
154164155165(qm_numba_result)=
156166157167```{code-cell} ipython3
158168with qe.Timer() as timer3:
159- qm_numba(0.1, int(n))
160-time3 = timer3.elapsed
169+ # Second run
170+ x = qm_numba(0.1, int(n))
161171```
162172173+Here's the speed gain
174+163175```{code-cell} ipython3
164-time1 / time3 # Calculate speed gain
176+timer1.elapsed / timer3.elapsed
165177```
166178179+This is a big boost for a small modification to our original code.
180+181+Let's discuss how this works.
167182168183### How and When it Works
169184170-Numba attempts to generate fast machine code using the infrastructure provided by the [LLVM Project](https://llvm.org/).
185+Numba attempts to generate fast machine code using the infrastructure provided
186+by the [LLVM Project](https://llvm.org/).
171187172188It does this by inferring type information on the fly.
173189174190(See our {doc}`earlier lecture <need_for_speed>` on scientific computing for a discussion of types.)
175191176192The basic idea is this:
177193178-* Python is very flexible and hence we could call the function qm with many
179- types.
194+* Python is very flexible and hence we could call the function qm with many types.
180195* e.g., `x0` could be a NumPy array or a list, `n` could be an integer or a float, etc.
181196* This makes it very difficult to generate efficient machine code *ahead of time* (i.e., before runtime).
182197* However, when we do actually *call* the function, say by running `qm(0.5, 10)`,
183- the types of `x0` and `n` become clear.
198+ the types of `x0`, `α` and `n` are determined.
184199* Moreover, the types of *other variables* in `qm` *can be inferred once the input types are known*.
185200* So the strategy of Numba and other JIT compilers is to *wait until the function is called*, and then compile.
186201187202That is called "just-in-time" compilation.
188203189-Note that, if you make the call `qm(0.5, 10)` and then follow it with `qm(0.9,
190-20)`, compilation only takes place on the first call.
204+Note that, if you make the call `qm_numba(0.5, 10)` and then follow it with `qm_numba(0.9, 20)`, compilation only takes place on the first call.
191205192206This is because compiled code is cached and reused as required.
193207194-This is why, in the code above, `time3` is smaller than `time2`.
208+This is why, in the code above, the second run of `qm_numba` is faster.
195209196210```{admonition} Remark
197-In practice, rather than writing `qm_numba = jit(qm)`, we use *decorator* syntax and put `@jit` before the function definition. This is equivalent to adding `qm = jit(qm)` after the definition. We use this syntax throughout the rest of the lecture. (See {doc}`python_advanced_features` for more on decorators.)
211+In practice, rather than writing `qm_numba = jit(qm)`, we typically use
212+*decorator* syntax and put `@jit` before the function definition. This is
213+equivalent to adding `qm = jit(qm)` after the definition.
198214```
199215200216201-## Type Inference
217+## Sharp Bits
202218203-Successful type inference is a key part of JIT compilation.
219+Numba is relatively easy to use but not always seamless.
204220205-As you can imagine, inferring types is easier for simple Python objects (e.g.,
206-simple scalar data types such as floats and integers).
221+Let's review some of the issues users run into.
207222208-Numba also plays well with NumPy arrays, which have well-defined types.
223+### Typing
209224210-In an ideal setting, Numba can infer all necessary type information.
225+Successful type inference is the key to JIT compilation.
211226212-This allows it to generate efficient native machine code, without having to call the Python runtime environment.
227+In an ideal setting, Numba can infer all necessary type information.
213228214-When Numba cannot infer all type information, it will raise an error.
229+When Numba *cannot* infer all type information, it will raise an error.
215230216-For example, in the setting below, Numba is unable to determine the type of the function `g` when compiling `iterate`
231+For example, in the setting below, Numba is unable to determine the type of the
232+function `g` when compiling `iterate`
217233218234```{code-cell} ipython3
219235@jit
@@ -234,7 +250,7 @@ except Exception as e:
234250 print(e)
235251```
236252237-We can fix this easily by compiling `g`.
253+In the present case, we can fix this easily by compiling `g`.
238254239255```{code-cell} ipython3
240256@jit
@@ -244,28 +260,16 @@ def g(x):
244260iterate(g, 0.5, 100)
245261```
246262263+In other cases, such as when we want to use functions from external libaries
264+such as `SciPy`, there might not be any easy workaround.
247265248-## Dangers and Limitations
249-250-Let's add some cautionary notes.
251-252-### Limitations
253-254-As we've seen, Numba needs to infer type information on
255-all variables to generate fast machine-level instructions.
256266257-For large routines or those using external libraries, this process can easily fail.
267+### Global Variables
258268259-Hence, it's best to focus on speeding up small, time-critical snippets of code.
269+Another thing to be careful about when using Numba is handling of global
270+variables.
260271261-This will give you much better performance than blanketing your Python programs with `@jit` statements.
262-263-264-### A Gotcha: Global Variables
265-266-Here's another thing to be careful about when using Numba.
267-268-Consider the following example
272+For example, consider the following code
269273270274```{code-cell} ipython3
271275a = 1
@@ -284,9 +288,10 @@ print(add_a(10))
284288```
285289286290Notice that changing the global had no effect on the value returned by the
287-function.
291+function 😱.
288292289-When Numba compiles machine code for functions, it treats global variables as constants to ensure type stability.
293+When Numba compiles machine code for functions, it treats global variables as
294+constants to ensure type stability.
290295291296To avoid this, pass values as function arguments rather than relying on globals.
292297@@ -320,15 +325,11 @@ Here's the code:
320325321326```{code-cell} ipython3
322327@jit
323-def h(w, r=0.1, s=0.3, v1=0.1, v2=1.0):
324- """
325- Updates household wealth.
326- """
327-328+def update(w, r=0.1, s=0.3, v1=0.1, v2=1.0):
329+ " Updates household wealth. "
328330 # Draw shocks
329331 R = np.exp(v1 * np.random.randn()) * (1 + r)
330332 y = np.exp(v2 * np.random.randn())
331-332333 # Update wealth
333334 w = R * s * w + y
334335 return w
@@ -343,7 +344,7 @@ T = 100
343344w = np.empty(T)
344345w[0] = 5
345346for t in range(T-1):
346- w[t+1] = h(w[t])
347+ w[t+1] = update(w[t])
347348348349ax.plot(w)
349350ax.set_xlabel('$t$', fontsize=12)
@@ -365,21 +366,30 @@ Here's the code:
365366```{code-cell} ipython3
366367@jit
367368def compute_long_run_median(w0=1, T=1000, num_reps=50_000):
368-369369 obs = np.empty(num_reps)
370+ # For each household
370371 for i in range(num_reps):
372+ # Set the initial condition and run forward in time
371373 w = w0
372374 for t in range(T):
373- w = h(w)
375+ w = update(w)
376+ # Record the final value
374377 obs[i] = w
375-378+ # Take the median of all final values
376379 return np.median(obs)
377380```
378381379382Let's see how fast this runs:
380383381384```{code-cell} ipython3
382385with qe.Timer():
386+ # Warm up
387+ compute_long_run_median()
388+```
389+390+```{code-cell} ipython3
391+with qe.Timer():
392+ # Second run
383393 compute_long_run_median()
384394```
385395@@ -391,22 +401,29 @@ To do so, we add the `parallel=True` flag and change `range` to `prange`:
391401from numba import prange
392402393403@jit(parallel=True)
394-def compute_long_run_median_parallel(w0=1, T=1000, num_reps=50_000):
395-404+def compute_long_run_median_parallel(
405+ w0=1, T=1000, num_reps=50_000
406+ ):
396407 obs = np.empty(num_reps)
397- for i in prange(num_reps):
408+ for i in prange(num_reps): # Parallelize over households
398409 w = w0
399410 for t in range(T):
400- w = h(w)
411+ w = update(w)
401412 obs[i] = w
402-403413 return np.median(obs)
404414```
405415406416Let's look at the timing:
407417408418```{code-cell} ipython3
409419with qe.Timer():
420+ # Warm up
421+ compute_long_run_median_parallel()
422+```
423+424+```{code-cell} ipython3
425+with qe.Timer():
426+ # Second run
410427 compute_long_run_median_parallel()
411428```
412429