Repository: numba/numba

Issue:
Error when profiling
Hi,

First, thanks for providing numba--it's a wonderful tool. However, when I went to profile an application using numba, the profiler actually dies. Here is a minimal example:

``` python
from numba import jit, float64
import numpy as np

@jit(float64(float64[:], float64[:]), nopython=True, nogil=True)
def get_dot(a, b):
    dot = np.dot(a, b)
    return dot

def run_numba():
    a = np.array([2.0,3.8,4.2])
    b = np.array([4.4, 2.4, 2.5])
    dot = get_dot(a, b)
    print(dot)

if __name__=="__main__":
    run_numba()
```

If I save this to `numba_profiling.py` and run it, I get exactly what I expected. However, if I run
`python -m cProfile -o numbaprof.prof numba_profiling.py`, I receive this error:

``` pytb
Fatal Python error: BLAS wrapper returned with an error
KeyError: '__builtins__'

Current thread 0x00007fff7cbfa300 (most recent call first):
  File "numba_profiling.py", line 4 in get_dot
  File "numba_profiling.py", line 13 in run_numba
  File "numba_profiling.py", line 18 in <module>
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/cProfile.py", line 100 in runctx
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/profile.py", line 64 in runctx
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/cProfile.py", line 20 in runctx
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/cProfile.py", line 153 in main
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/cProfile.py", line 160 in <module>
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/runpy.py", line 85 in _run_code
  File "/Users/grinawap/anaconda/envs/openeye-works/lib/python3.4/runpy.py", line 170 in _run_module_as_main
Abort trap: 6
```

I looked around a bit, and could not find anything about this error.

I understand that the Python profiler can't see inside compiled functions, but I would imagine that it should not be yielding this error. If you've seen this and it's a problem with BLAS on my side, my apologies for the bother. If you need any more information, please let me know.

Thanks again for a wonderful product, and sorry to bother if this is something trivial!



Discussion:

Resolving patch:
diff --git a/numba/targets/linalg.py b/numba/targets/linalg.py
--- a/numba/targets/linalg.py
+++ b/numba/targets/linalg.py
@@ -4,6 +4,8 @@
 
 from __future__ import print_function, absolute_import, division
 
+import contextlib
+
 from llvmlite import ir
 import numpy
 
@@ -45,14 +47,17 @@ def make_constant_slot(context, builder, ty, val):
     const = context.get_constant_generic(builder, ty, val)
     return cgutils.alloca_once_value(builder, const)
 
+
+@contextlib.contextmanager
 def make_contiguous(context, builder, sig, args):
     """
     Ensure that all array arguments are contiguous, if necessary by
     copying them.
-    A new (sig, args) tuple is returned.
+    A new (sig, args) tuple is yielded.
     """
     newtys = []
     newargs = []
+    copies = []
     for ty, val in zip(sig.args, args):
         if not isinstance(ty, types.Array) or ty.layout in 'CF':
             newty, newval = ty, val
@@ -60,9 +65,12 @@ def make_contiguous(context, builder, sig, args):
             newty = ty.copy(layout='C')
             copysig = signature(newty, ty)
             newval = array_copy(context, builder, copysig, (val,))
+            copies.append((newty, newval))
         newtys.append(newty)
         newargs.append(newval)
-    return signature(sig.return_type, *newtys), tuple(newargs)
+    yield signature(sig.return_type, *newtys), tuple(newargs)
+    for ty, val in copies:
+        context.nrt_decref(builder, ty, val)
 
 
 def check_c_int(context, builder, n):
@@ -286,19 +294,19 @@ def dot_2(context, builder, sig, args):
     a @ b
     """
     ensure_blas()
-    sig, args = make_contiguous(context, builder, sig, args)
-
-    ndims = [x.ndim for x in sig.args[:2]]
-    if ndims == [2, 2]:
-        return dot_2_mm(context, builder, sig, args)
-    elif ndims == [2, 1]:
-        return dot_2_mv(context, builder, sig, args)
-    elif ndims == [1, 2]:
-        return dot_2_vm(context, builder, sig, args)
-    elif ndims == [1, 1]:
-        return dot_2_vv(context, builder, sig, args)
-    else:
-        assert 0
+
+    with make_contiguous(context, builder, sig, args) as (sig, args):
+        ndims = [x.ndim for x in sig.args[:2]]
+        if ndims == [2, 2]:
+            return dot_2_mm(context, builder, sig, args)
+        elif ndims == [2, 1]:
+            return dot_2_mv(context, builder, sig, args)
+        elif ndims == [1, 2]:
+            return dot_2_vm(context, builder, sig, args)
+        elif ndims == [1, 1]:
+            return dot_2_vv(context, builder, sig, args)
+        else:
+            assert 0
 
 @lower_builtin(numpy.vdot, types.Array, types.Array)
 def vdot(context, builder, sig, args):
@@ -306,9 +314,9 @@ def vdot(context, builder, sig, args):
     np.vdot(a, b)
     """
     ensure_blas()
-    sig, args = make_contiguous(context, builder, sig, args)
 
-    return dot_2_vv(context, builder, sig, args, conjugate=True)
+    with make_contiguous(context, builder, sig, args) as (sig, args):
+        return dot_2_vv(context, builder, sig, args, conjugate=True)
 
 
 def dot_3_vm(context, builder, sig, args):
@@ -455,15 +463,15 @@ def dot_3(context, builder, sig, args):
     np.dot(a, b, out)
     """
     ensure_blas()
-    sig, args = make_contiguous(context, builder, sig, args)
 
-    ndims = set(x.ndim for x in sig.args[:2])
-    if ndims == set([2]):
-        return dot_3_mm(context, builder, sig, args)
-    elif ndims == set([1, 2]):
-        return dot_3_vm(context, builder, sig, args)
-    else:
-        assert 0
+    with make_contiguous(context, builder, sig, args) as (sig, args):
+        ndims = set(x.ndim for x in sig.args[:2])
+        if ndims == set([2]):
+            return dot_3_mm(context, builder, sig, args)
+        elif ndims == set([1, 2]):
+            return dot_3_vm(context, builder, sig, args)
+        else:
+            assert 0
 
 
 def call_xxgetrf(context, builder, a_type, a_shapes, a_data, ipiv, info):
@@ -545,7 +553,7 @@ def mat_inv(context, builder, sig, args):
     # Scipy for which one can specify to whether or not to overwrite the
     # input).
     def create_out(a):
-        m, n= a.shape
+        m, n = a.shape
         if m != n:
             raise ValueError("np.linalg.inv can only work on square "
                              "arrays.")
@@ -572,15 +580,14 @@ def create_out(a):
     invalid_arg = builder.icmp_signed('<', info_val, zero)
 
     with builder.if_then(lapack_error, False):
+        context.nrt_decref(builder, ipiv_t, ipiv)
         with builder.if_else(invalid_arg) as (then, otherwise):
             raise_err = context.call_conv.return_user_exc
             with then:
-                context.nrt_decref(builder, ipiv_t, ipiv)
                 raise_err(builder, ValueError,
                           ('One argument passed to getrf is invalid',)
                           )
             with otherwise:
-                context.nrt_decref(builder, ipiv_t, ipiv)
                 raise_err(builder, ValueError,
                           ('Matrix is singular and cannot be inverted',)
                           )


Test patch:
diff --git a/numba/tests/support.py b/numba/tests/support.py
--- a/numba/tests/support.py
+++ b/numba/tests/support.py
@@ -9,6 +9,7 @@
 import math
 import os
 import shutil
+import subprocess
 import sys
 import tempfile
 import time
@@ -102,7 +103,7 @@ def assertRefCount(self, *objects):
         """
         A context manager that asserts the given objects have the
         same reference counts before and after executing the
-        enclosed blocks.
+        enclosed block.
         """
         old_refcounts = [sys.getrefcount(x) for x in objects]
         yield
@@ -112,6 +113,25 @@ def assertRefCount(self, *objects):
                 self.fail("Refcount changed from %d to %d for object: %r"
                           % (old, new, obj))
 
+    @contextlib.contextmanager
+    def assertNoNRTLeak(self):
+        """
+        A context manager that asserts no NRT leak was created during
+        the execution of the enclosed block.
+        """
+        old = rtsys.get_allocation_stats()
+        yield
+        new = rtsys.get_allocation_stats()
+        total_alloc = new.alloc - old.alloc
+        total_free = new.free - old.free
+        total_mi_alloc = new.mi_alloc - old.mi_alloc
+        total_mi_free = new.mi_free - old.mi_free
+        self.assertEqual(total_alloc, total_free,
+                         "number of data allocs != number of data frees")
+        self.assertEqual(total_mi_alloc, total_mi_free,
+                         "number of meminfo allocs != number of meminfo frees")
+
+
     _exact_typesets = [(bool, np.bool_), utils.INT_TYPES, (str,), (np.integer,), (utils.text_type), ]
     _approx_typesets = [(float,), (complex,), (np.inexact)]
     _sequence_typesets = [(tuple, list)]
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -1,6 +1,7 @@
 from __future__ import division, print_function
 
 import contextlib
+import gc
 from itertools import product
 import sys
 import warnings
@@ -38,6 +39,10 @@ class TestProduct(TestCase):
 
     dtypes = (np.float64, np.float32, np.complex128, np.complex64)
 
+    def setUp(self):
+        # Collect leftovers from previous test cases before checking for leaks
+        gc.collect()
+
     def sample_vector(self, n, dtype):
         # Be careful to generate only exactly representable float values,
         # to avoid rounding discrepancies between Numpy and Numba
@@ -66,16 +71,20 @@ def check_contiguity_warning(self, pyfunc):
         self.assertEqual(w[0].lineno, pyfunc.__code__.co_firstlineno + 1)
 
     def check_func(self, pyfunc, cfunc, args):
-        expected = pyfunc(*args)
-        got = cfunc(*args)
-        self.assertPreciseEqual(got, expected, ignore_sign_on_zero=True)
+        with self.assertNoNRTLeak():
+            expected = pyfunc(*args)
+            got = cfunc(*args)
+            self.assertPreciseEqual(got, expected, ignore_sign_on_zero=True)
+            del got, expected
 
     def check_func_out(self, pyfunc, cfunc, args, out):
-        expected = np.copy(out)
-        got = np.copy(out)
-        self.assertIs(pyfunc(*args, out=expected), expected)
-        self.assertIs(cfunc(*args, out=got), got)
-        self.assertPreciseEqual(got, expected, ignore_sign_on_zero=True)
+        with self.assertNoNRTLeak():
+            expected = np.copy(out)
+            got = np.copy(out)
+            self.assertIs(pyfunc(*args, out=expected), expected)
+            self.assertIs(cfunc(*args, out=got), got)
+            self.assertPreciseEqual(got, expected, ignore_sign_on_zero=True)
+            del got, expected
 
     def assert_mismatching_sizes(self, cfunc, args, is_out=False):
         with self.assertRaises(ValueError) as raises:
@@ -301,6 +310,10 @@ class TestLinalgInv(TestCase):
 
     dtypes = (np.float64, np.float32, np.complex128, np.complex64)
 
+    def setUp(self):
+        # Collect leftovers from previous test cases before checking for leaks
+        gc.collect()
+
     def sample_vector(self, n, dtype):
         # Be careful to generate only exactly representable float values,
         # to avoid rounding discrepancies between Numpy and Numba
@@ -344,21 +357,23 @@ def test_linalg_inv(self):
         """
         n = 10
         cfunc = jit(nopython=True)(invert_matrix)
+
+        def check(a, **kwargs):
+            with self.assertNoNRTLeak():
+                expected = invert_matrix(a).copy(order='C')
+                got = cfunc(a)
+                # XXX had to use that function otherwise comparison fails
+                # because of +0, -0 discrepancies
+                np.testing.assert_array_almost_equal_nulp(got, expected, **kwargs)
+                del got, expected
+
         for dtype, order in product(self.dtypes, 'CF'):
             a = self.sample_matrix(n, dtype, order)
-            expected = invert_matrix(a).copy(order='C')
-            got = cfunc(a)
-            # XXX add to use that function otherwise comparison fails
-            # because of +0, -0 discrepancies
-            np.testing.assert_array_almost_equal_nulp(got, expected, nulp=3)
+            check(a, nulp=3)
 
         for order in 'CF':
             a = np.array(((2, 1), (2, 3)), dtype=np.float64, order=order)
-            expected = invert_matrix(a).copy(order='C')
-            got = cfunc(a)
-            # XXX add to use that function otherwise comparison fails
-            # because of +0, -0 discrepancies
-            np.testing.assert_array_almost_equal_nulp(got, expected)
+            check(a)
 
         # Non square matrices
         self.assert_non_square(cfunc, (np.ones((2,3)),))
diff --git a/numba/tests/test_profiler.py b/numba/tests/test_profiler.py
--- a/numba/tests/test_profiler.py
+++ b/numba/tests/test_profiler.py
@@ -1,35 +1,83 @@
-import numpy as np
 import cProfile as profiler
+import os
 import pstats
+import subprocess
+import sys
+
+import numpy as np
+
 from numba import jit
 from numba import unittest_support as unittest
+from .test_linalg import needs_blas
+
 
 def dot(a, b):
-    sum=0
+    sum = 0
     for i in range(len(a)):
         sum += a[i]*b[i]
     return sum
 
+def np_dot(a, b):
+    return np.dot(a, b)
 
-class TestProfiler(unittest.TestCase):
 
-    def test_profiler(self):
-        """Make sure the jit-compiled function shows up in the profile stats."""
+class TestProfiler(unittest.TestCase):
 
+    def check_profiler_dot(self, pyfunc):
+        """
+        Make sure the jit-compiled function shows up in the profile stats
+        as a regular Python function.
+        """
         a = np.arange(16, dtype=np.float32)
         b = np.arange(16, dtype=np.float32)
+        cfunc = jit(nopython=True)(pyfunc)
+        # Warm up JIT
+        cfunc(a, b)
         p = profiler.Profile()
+        p.enable()
         try:
-            p.enable()
-            dot(a, b)
-            p.disable()
-            stats = pstats.Stats(p).strip_dirs()
-            self.assertIn(('test_profiler.py', 7, 'dot'), stats.stats)
+            cfunc(a, b)
         finally:
-            # make sure the profiler is deactivated when this test is done so as not to
-            # pollute any other tests
             p.disable()
-            del p
+        stats = pstats.Stats(p).strip_dirs()
+        code = pyfunc.__code__
+        expected_key = (os.path.basename(code.co_filename),
+                        code.co_firstlineno,
+                        code.co_name,
+                        )
+        self.assertIn(expected_key, stats.stats)
+
+    # The feature is currently broken.  We merely run the code to check
+    # Numba doesn't crash.
+
+    @unittest.expectedFailure
+    def test_profiler(self):
+        self.check_profiler_dot(dot)
+
+    @needs_blas
+    def test_profiler_np_dot(self):
+        # Issue #1786: initializing BLAS would crash when profiling
+        code = """if 1:
+            import cProfile as profiler
+
+            import numpy as np
+
+            from numba import jit
+            from numba.tests.test_profiler import np_dot
+
+            cfunc = jit(nopython=True)(np_dot)
+
+            a = np.arange(16, dtype=np.float32)
+            b = np.arange(16, dtype=np.float32)
+
+            p = profiler.Profile()
+            p.enable()
+            cfunc(a, b)
+            cfunc(a, b)
+            p.disable()
+            """
+        subprocess.check_call([sys.executable, "-c", code])
+
 
 if __name__ == '__main__':
     unittest.main()