You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
To speed the function `qm` up using Numba, our first step is
118
+
Let's see how long this takes to run for large $n$
121
119
122
120
```{code-cell} ipython3
123
-
from numba import jit
121
+
n = 10_000_000
122
+
123
+
with qe.Timer() as timer1:
124
+
# Time Python base version
125
+
x = qm(0.1, int(n))
124
126
125
-
qm_numba = jit(qm)
126
127
```
127
128
128
-
The function `qm_numba` is a version of `qm` that is "targeted" for
129
-
JIT-compilation.
130
129
131
-
We will explain what this means momentarily.
130
+
#### Acceleration via Numba
131
+
132
+
To speed the function `qm` up using Numba, we first import the `jit` function
132
133
133
-
Let's time and compare identical function calls across these two versions, starting with the original function `qm`:
134
134
135
135
```{code-cell} ipython3
136
-
n = 10_000_000
136
+
from numba import jit
137
+
```
137
138
138
-
with qe.Timer() as timer1:
139
-
qm(0.1, int(n))
140
-
time1 = timer1.elapsed
139
+
Now we apply it to `qm`, producing a new function:
140
+
141
+
```{code-cell} ipython3
142
+
qm_numba = jit(qm)
141
143
```
142
144
143
-
Now let's try qm_numba
145
+
The function `qm_numba` is a version of `qm` that is "targeted" for
146
+
JIT-compilation.
147
+
148
+
We will explain what this means momentarily.
149
+
150
+
Let's time this new version:
144
151
145
152
```{code-cell} ipython3
146
153
with qe.Timer() as timer2:
147
-
qm_numba(0.1, int(n))
148
-
time2 = timer2.elapsed
154
+
# Time jitted version
155
+
x = qm_numba(0.1, int(n))
149
156
```
150
157
151
-
This is already a very large speed gain.
158
+
This is a large speed gain.
152
159
153
-
In fact, the next time and all subsequent times it runs even faster as the function has been compiled and is in memory:
160
+
In fact, the next time and all subsequent times it runs even faster as the
161
+
function has been compiled and is in memory:
154
162
155
163
(qm_numba_result)=
156
164
157
165
```{code-cell} ipython3
158
166
with qe.Timer() as timer3:
159
-
qm_numba(0.1, int(n))
160
-
time3 = timer3.elapsed
167
+
# Second run
168
+
x = qm_numba(0.1, int(n))
161
169
```
162
170
171
+
Here's the speed gain
172
+
163
173
```{code-cell} ipython3
164
-
time1 / time3 # Calculate speed gain
174
+
timer1.elapsed / timer3.elapsed
165
175
```
166
176
177
+
This is a big boost for a small modification to our original code.
178
+
179
+
Let's discuss how this works.
167
180
168
181
### How and When it Works
169
182
170
-
Numba attempts to generate fast machine code using the infrastructure provided by the [LLVM Project](https://llvm.org/).
183
+
Numba attempts to generate fast machine code using the infrastructure provided
184
+
by the [LLVM Project](https://llvm.org/).
171
185
172
186
It does this by inferring type information on the fly.
173
187
174
188
(See our {doc}`earlier lecture <need_for_speed>` on scientific computing for a discussion of types.)
175
189
176
190
The basic idea is this:
177
191
178
-
* Python is very flexible and hence we could call the function qm with many
179
-
types.
192
+
* Python is very flexible and hence we could call the function qm with many types.
180
193
* e.g., `x0` could be a NumPy array or a list, `n` could be an integer or a float, etc.
181
194
* This makes it very difficult to generate efficient machine code *ahead of time* (i.e., before runtime).
182
195
* However, when we do actually *call* the function, say by running `qm(0.5, 10)`,
183
-
the types of `x0`and `n`become clear.
196
+
the types of `x0`, `α`and `n`are determined.
184
197
* Moreover, the types of *other variables* in `qm`*can be inferred once the input types are known*.
185
198
* So the strategy of Numba and other JIT compilers is to *wait until the function is called*, and then compile.
186
199
187
200
That is called "just-in-time" compilation.
188
201
189
-
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.
202
+
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.
191
203
192
204
This is because compiled code is cached and reused as required.
193
205
194
-
This is why, in the code above, `time3` is smaller than `time2`.
206
+
This is why, in the code above, the second run of `qm_numba` is faster.
195
207
196
208
```{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.)
209
+
In practice, rather than writing `qm_numba = jit(qm)`, we typically use
210
+
*decorator* syntax and put `@jit` before the function definition. This is
211
+
equivalent to adding `qm = jit(qm)` after the definition.
198
212
```
199
213
200
214
201
-
## Type Inference
215
+
## Sharp Bits
202
216
203
-
Successful type inference is a key part of JIT compilation.
217
+
Numba is relatively easy to use but not always seamless.
204
218
205
-
As you can imagine, inferring types is easier for simple Python objects (e.g.,
206
-
simple scalar data types such as floats and integers).
219
+
Let's review some of the issues users run into.
207
220
208
-
Numba also plays well with NumPy arrays, which have well-defined types.
221
+
### Typing
209
222
210
-
In an ideal setting, Numba can infer all necessary type information.
223
+
Successful type inference is the key to JIT compilation.
211
224
212
-
This allows it to generate efficient native machine code, without having to call the Python runtime environment.
225
+
In an ideal setting, Numba can infer all necessary type information.
213
226
214
-
When Numba cannot infer all type information, it will raise an error.
227
+
When Numba *cannot* infer all type information, it will raise an error.
215
228
216
-
For example, in the setting below, Numba is unable to determine the type of the function `g` when compiling `iterate`
229
+
For example, in the setting below, Numba is unable to determine the type of the
230
+
function `g` when compiling `iterate`
217
231
218
232
```{code-cell} ipython3
219
233
@jit
@@ -234,7 +248,7 @@ except Exception as e:
234
248
print(e)
235
249
```
236
250
237
-
We can fix this easily by compiling `g`.
251
+
In the present case, we can fix this easily by compiling `g`.
238
252
239
253
```{code-cell} ipython3
240
254
@jit
@@ -244,28 +258,16 @@ def g(x):
244
258
iterate(g, 0.5, 100)
245
259
```
246
260
261
+
In other cases, such as when we want to use functions from external libaries
262
+
such as `SciPy`, there might not be any easy workaround.
247
263
248
-
## Dangers and Limitations
249
-
250
-
Let's add some cautionary notes.
251
-
252
-
### Limitations
253
264
254
-
As we've seen, Numba needs to infer type information on
255
-
all variables to generate fast machine-level instructions.
265
+
### Global Variables
256
266
257
-
For large routines or those using external libraries, this process can easily fail.
267
+
Another thing to be careful about when using Numba is handling of global
268
+
variables.
258
269
259
-
Hence, it's best to focus on speeding up small, time-critical snippets of code.
260
-
261
-
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
270
+
For example, consider the following code
269
271
270
272
```{code-cell} ipython3
271
273
a = 1
@@ -284,9 +286,10 @@ print(add_a(10))
284
286
```
285
287
286
288
Notice that changing the global had no effect on the value returned by the
287
-
function.
289
+
function 😱.
288
290
289
-
When Numba compiles machine code for functions, it treats global variables as constants to ensure type stability.
291
+
When Numba compiles machine code for functions, it treats global variables as
292
+
constants to ensure type stability.
290
293
291
294
To avoid this, pass values as function arguments rather than relying on globals.
0 commit comments