3

In python you can define multiple functions that call each other in any order, and at runtime the functions will be called on. The order that these functions are defined in a script doesn't matter, once they exist. For example, the below is valid and will work

import numpy as np

def func1(arr):
    out = np.empty_like(arr)
    for i in range(arr.shape[0]):
        out[i] = func2(arr[i])  # calling func2 here which is defined below
    return out

def func2(a):
    out = a + 1
    return out

func1 can call on func2 even though func2 is defined after func1.

However, if I decorate these functions with numba, I get an error

import numpy as np
import numba as nb


@nb.jit("f8[:](f8[:])", nopython=True)
def func1(arr):
    out = np.empty_like(arr)
    for i in range(arr.shape[0]):
        out[i] = func2(arr[i])
    return out

@nb.jit("f8(f8)", nopython=True)
def func2(a):
    out = a + 1
    return out

>>> TypingError: Failed in nopython mode pipeline (step: nopython frontend)
    Untyped global name 'func2': cannot determine Numba type of <class 
    'numba.ir.UndefinedType'>

So numba doesn't know what func2 is when compiling func1 using JIT. Simply switching the order of these functions works though, so that func2 comes before func1

@nb.jit("f8(f8)", nopython=True)
def func2(a):
    out = a + 1
    return out

@nb.jit("f8[:](f8[:])", nopython=True)
def func1(arr):
    out = np.empty_like(arr)
    for i in range(arr.shape[0]):
        out[i] = func2(arr[i])
    return out

Why is this? I have a feeling the pure python mode works because python is dynamically typed and not compiled, whereas numba, using JIT, by definition does compile the functions (and maybe therefore needs perfect knowledge of everything that happens within every function?). But I don't understand why numba doesn't search within the scope for all functions if it comes across a function it hasn't seen.

PyRsquared
  • 6,970
  • 11
  • 50
  • 86

1 Answers1

4

Short version - delete the "f8[:](f8[:])"

Your intuition is right. Python functions are looked up at call time, which is why they can be defined out of order. Looking at the python bytecode with a dis (disassembly) module make this clear - the name b is looked up as a global each time function a is called.

def a():
    return b()

def b():
    return 2

import dis
dis.dis(a)
#  2           0 LOAD_GLOBAL              0 (b)
#              2 CALL_FUNCTION            0
#              4 RETURN_VALUE

In nopython mode, numba needs to statically know the address of each function that is being called - this makes the code fast (no longer doing a runtime lookup), and also opens the door to other optimizations, like inlining.

That said, numba can handle this case. By specifying the type signature ("f8[:](f8[:])"), you are forcing ahead of time compilation. Omit it, and a number will defer to the first function call it and it will work.

chrisb
  • 49,833
  • 8
  • 70
  • 70
  • Thanks for the explanation. I did notice that by omitting the signature (but still decorating the function with jit) seemed to work. However for my use case I'll leave the signature because I want to get the proper error messages incase some incorrect type is passed to the function before I start operating on it. Thanks again! – PyRsquared Mar 28 '19 at 20:22
  • 1
    @PyRsquared Please note also that explicitly declaring non contiguous arrays [:] usually prevents SIMD-vectorization and can lead to slower runtimes (just replace `out = a + 1` by `out = np.sin(a)` to see a quite noticeable effect. But if you explicitly declare contiguous arrays [::1] the function won't work on non-contigous arrays anymore.... – max9111 Mar 29 '19 at 08:20
  • @max9111 Excellent observation, thanks! So in this case here, for optimal performance (using SIMD), you suggest using `f[::1]` ? I'm not entirely sure how numpy arrays are stored in memory, but for [1D case surely they are contiguous](https://stackoverflow.com/a/26999092/4139143)? – PyRsquared Mar 29 '19 at 10:40
  • 1
    @PyRsquared You can check this things with ndarray.flags (eg. A=B[0:-1:2] -> A is a non contiguous view on B). Well you have to choose it. If it is OK for you limit the function to contiguous arrays... Another possibility is to use np.ascontiguousarray, but this has the downside of copying an array. Sometimes you want to explicitly create a contiguous copy eg. https://stackoverflow.com/a/55405644/4045774, sometimes not. It depends on the algorithm... – max9111 Mar 29 '19 at 14:01