Repository: numba/numba

Issue:
Failure in CUDA tests
I get the following failure deterministically when running `./runtests.py -vf numba.cuda.tests` (although the exact integer value varies from run to run):

```
======================================================================
ERROR: test_cuda_driver_occupancy (numba.cuda.tests.cudadrv.test_cuda_driver.TestCudaDriver)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/antoine/numba/numba/cuda/tests/cudadrv/test_cuda_driver.py", line 117, in test_cuda_driver_occupancy
    module = self.context.create_module_ptx(self.ptx)
  File "/home/antoine/numba/numba/cuda/cudadrv/driver.py", line 646, in create_module_ptx
    return self.create_module_image(image)
  File "/home/antoine/numba/numba/cuda/cudadrv/driver.py", line 649, in create_module_image
    self.trashing.service()
  File "/home/antoine/numba/numba/servicelib/service.py", line 30, in service
    next(self._task)
  File "/home/antoine/numba/numba/cuda/cudadrv/driver.py", line 321, in process
    cb()
  File "/home/antoine/numba/numba/cuda/cudadrv/driver.py", line 779, in cleanup
    del modules[handle.value]
KeyError: 47652304
```



Discussion:
According to `git bisect`, this was introduced in 0d67b645aadc667e26d3243d2e48779f8394524f.


Resolving patch:
diff --git a/numba/cuda/cudadrv/driver.py b/numba/cuda/cudadrv/driver.py
--- a/numba/cuda/cudadrv/driver.py
+++ b/numba/cuda/cudadrv/driver.py
@@ -14,7 +14,6 @@
 from __future__ import absolute_import, print_function, division
 import sys
 import os
-import traceback
 import ctypes
 import weakref
 import functools
@@ -145,6 +144,7 @@ def _build_reverse_error_map():
             map[code] = name
     return map
 
+
 def _getpid():
     return os.getpid()
 
@@ -359,7 +359,7 @@ def __init__(self, devnum):
         driver.cuDeviceGet(byref(got_devnum), devnum)
         assert devnum == got_devnum.value, "Driver returned another device"
         self.id = got_devnum.value
-        self.trashing = TrashService("cuda.device%d.trash" % self.id)
+        self.trashing = trashing = TrashService("cuda.device%d.trash" % self.id)
         self.attributes = {}
         # Read compute capability
         cc_major = c_int()
@@ -373,6 +373,11 @@ def __init__(self, devnum):
         driver.cuDeviceGetName(buf, bufsz, self.id)
         self.name = buf.value
 
+        def finalizer():
+            trashing.clear()
+
+        utils.finalize(self, finalizer)
+
     @property
     def COMPUTE_CAPABILITY(self):
         """
@@ -382,12 +387,6 @@ def COMPUTE_CAPABILITY(self):
                       "case version", DeprecationWarning)
         return self.compute_capability
 
-    def __del__(self):
-        try:
-            self.reset()
-        except:
-            traceback.print_exc()
-
     def __repr__(self):
         return "<CUDA device %d '%s'>" % (self.id, self.name)
 
@@ -469,7 +468,7 @@ def __init__(self, device, handle, finalizer=None):
                                      (self.device.id, self.handle.value))
         self.allocations = utils.UniqueDict()
         self.modules = utils.UniqueDict()
-        self.finalizer = utils.finalize(self, self._make_finalizer())
+        utils.finalize(self, self._make_finalizer())
         # For storing context specific data
         self.extras = {}
 
@@ -481,6 +480,7 @@ def _make_finalizer(self):
         modules = self.modules
         trashing = self.trashing
         external_finalizer = self.external_finalizer
+
         def finalize():
             allocations.clear()
             modules.clear()
@@ -523,7 +523,7 @@ def get_active_blocks_per_multiprocessor(self, func, blocksize, memsize, flags=N
     def get_max_potential_block_size(self, func, b2d_func, memsize, blocksizelimit, flags=None):
         """Suggest a launch configuration with reasonable occupancy.
         :param func: kernel for which occupancy is calculated
-        :param b2d_func: function that calculates how much per-block dynamic shared memory 'func' 
+        :param b2d_func: function that calculates how much per-block dynamic shared memory 'func'
           uses based on the block size.
         :param memsize: per-block dynamic shared memory usage intended, in bytes
         :param blocksizelimit: maximum block size the kernel is designed to handle"""
@@ -596,13 +596,6 @@ def memhostalloc(self, bytesize, mapped=False, portable=False, wc=False):
                                finalizer=finalizer)
             return mem
 
-    def memfree(self, pointer):
-        try:
-            del self.allocations[pointer.value]
-        except KeyError:
-            raise DeadMemoryError
-        self.trashing.service()
-
     def mempin(self, owner, pointer, size, mapped=False):
         self.trashing.service()
 
@@ -635,7 +628,6 @@ def mempin(self, owner, pointer, size, mapped=False):
                                                            pointer))
             return mem
 
-
     def memunpin(self, pointer):
         raise NotImplementedError
 
@@ -775,8 +767,7 @@ def _module_finalizer(context, handle):
 
     def core():
         def cleanup():
-            if modules:
-                del modules[handle.value]
+            assert handle.value not in modules
             driver.cuModuleUnload(handle)
 
         trashing.add_trash(cleanup)
@@ -787,23 +778,22 @@ def cleanup():
 class MemoryPointer(object):
     __cuda_memory__ = True
 
-    def __init__(self, context, pointer, size, finalizer=None):
+    def __init__(self, context, pointer, size, finalizer=None, owner=None):
         self.context = context
         self.device_pointer = pointer
         self.size = size
         self._cuda_memsize_ = size
-        self.finalizer = finalizer
         self.is_managed = finalizer is not None
-        self.is_alive = True
         self.refct = 0
         self.handle = self.device_pointer
+        self._owner = owner
 
-    def __del__(self):
-        try:
-            if self.is_managed and self.is_alive:
-                self.finalizer()
-        except:
-            traceback.print_exc()
+        if finalizer is not None:
+            self._finalizer = utils.finalize(self, finalizer)
+
+    @property
+    def owner(self):
+        return self if self._owner is None else self._owner
 
     def own(self):
         return OwnedPointer(weakref.proxy(self))
@@ -813,10 +803,10 @@ def free(self):
         Forces the device memory to the trash.
         """
         if self.is_managed:
-            if not self.is_alive:
+            if not self._finalizer.alive:
                 raise RuntimeError("Freeing dead memory")
-            self.finalizer()
-            self.is_alive = False
+            self._finalizer()
+            assert not self._finalizer.alive
 
     def memset(self, byte, count=None, stream=0):
         count = self.size if count is None else count
@@ -834,8 +824,8 @@ def view(self, start, stop=None):
             size = stop - start
         assert size > 0, "zero or negative memory size"
         pointer = drvapi.cu_device_ptr(base)
-        view = MemoryPointer(self.context, pointer, size)
-        return OwnedPointer(weakref.proxy(self), view)
+        view = MemoryPointer(self.context, pointer, size, owner=self.owner)
+        return OwnedPointer(weakref.proxy(self.owner), view)
 
     @property
     def device_ctypes_pointer(self):
@@ -871,26 +861,14 @@ def __init__(self, context, owner, pointer, size, finalizer=None):
         self.size = size
         self.host_pointer = pointer
         self.is_managed = finalizer is not None
-        self.finalizer = finalizer
-        self.is_alive = True
         self.handle = self.host_pointer
 
         # For buffer interface
         self._buflen_ = self.size
         self._bufptr_ = self.host_pointer.value
 
-    def __del__(self):
-        try:
-            if self.is_managed and self.is_alive:
-                self.finalizer()
-        except:
-            traceback.print_exc()
-
-    def unpin(self):
-        if not self.is_alive:
-            raise DeadMemoryError
-        self.finalizer()
-        self.is_alive = False
+        if finalizer is not None:
+            utils.finalize(self, finalizer)
 
     def own(self):
         return self
@@ -899,23 +877,23 @@ def own(self):
 class OwnedPointer(object):
     def __init__(self, memptr, view=None):
         self._mem = memptr
-        self._mem.refct += 1
+
         if view is None:
             self._view = self._mem
         else:
             assert not view.is_managed
             self._view = view
 
-    def __del__(self):
-        try:
-            self._mem.refct -= 1
-            assert self._mem.refct >= 0
-            if self._mem.refct == 0:
-                self._mem.free()
-        except ReferenceError:
-            pass
-        except:
-            traceback.print_exc()
+        mem = self._mem
+
+        def deref():
+            mem.refct -= 1
+            assert mem.refct >= 0
+            if mem.refct == 0:
+                mem.free()
+
+        self._mem.refct += 1
+        utils.finalize(self, deref)
 
     def __getattr__(self, fname):
         """Proxy MemoryPointer methods
@@ -931,15 +909,8 @@ class Stream(object):
     def __init__(self, context, handle, finalizer):
         self.context = context
         self.handle = handle
-        self.finalizer = finalizer
-        self.is_managed = finalizer is not None
-
-    def __del__(self):
-        try:
-            if self.is_managed:
-                self.finalizer()
-        except:
-            traceback.print_exc()
+        if finalizer is not None:
+            utils.finalize(self, finalizer)
 
     def __int__(self):
         return self.handle.value
@@ -968,15 +939,8 @@ class Event(object):
     def __init__(self, context, handle, finalizer=None):
         self.context = context
         self.handle = handle
-        self.finalizer = finalizer
-        self.is_managed = self.finalizer is not None
-
-    def __del__(self):
-        try:
-            if self.is_managed:
-                self.finalizer()
-        except:
-            traceback.print_exc()
+        if finalizer is not None:
+            utils.finalize(self, finalizer)
 
     def query(self):
         """
@@ -1037,15 +1001,8 @@ def __init__(self, context, handle, info_log, finalizer=None):
         self.context = context
         self.handle = handle
         self.info_log = info_log
-        self.finalizer = finalizer
-        self.is_managed = self.finalizer is not None
-
-    def __del__(self):
-        try:
-            if self.is_managed:
-                self.finalizer()
-        except:
-            traceback.print_exc()
+        if finalizer is not None:
+            self._finalizer = utils.finalize(self, finalizer)
 
     def unload(self):
         self.context.unload_module(self)
@@ -1103,7 +1060,7 @@ def configure(self, griddim, blockdim, sharedmem=0, stream=0):
         while len(blockdim) < 3:
             blockdim += (1,)
 
-        inst = copy.copy(self) # shallow clone the object
+        inst = copy.copy(self)  # shallow clone the object
         inst.griddim = griddim
         inst.blockdim = blockdim
         inst.sharedmem = sharedmem
@@ -1204,7 +1161,7 @@ def __init__(self):
         driver.cuLinkCreate(len(raw_keys), option_keys, option_vals,
                             byref(self.handle))
 
-        self.finalizer = lambda: driver.cuLinkDestroy(handle)
+        utils.finalize(self, driver.cuLinkDestroy, handle)
 
         self.linker_info_buf = linkerinfo
         self.linker_errors_buf = linkererrors
@@ -1219,12 +1176,6 @@ def info_log(self):
     def error_log(self):
         return self.linker_errors_buf.value.decode('utf8')
 
-    def __del__(self):
-        try:
-            self.finalizer()
-        except:
-            traceback.print_exc()
-
     def add_ptx(self, ptx, name='<cudapy-ptx>'):
         ptxbuf = c_char_p(ptx)
         namebuf = c_char_p(name.encode('utf8'))


Test patch:
diff --git a/numba/cuda/tests/cudadrv/test_cuda_memory.py b/numba/cuda/tests/cudadrv/test_cuda_memory.py
--- a/numba/cuda/tests/cudadrv/test_cuda_memory.py
+++ b/numba/cuda/tests/cudadrv/test_cuda_memory.py
@@ -40,6 +40,30 @@ def test_pinned_memory(self):
                                      mapped=True)
         self._template(devmem)
 
+    def test_derived_pointer(self):
+        # Use MemoryPointer.view to create derived pointer
+        def check(m, offset):
+            # create view
+            v1 = m.view(offset)
+            self.assertEqual(v1.owner.handle.value, m.handle.value)
+            self.assertEqual(m.refct, 2)
+            self.assertEqual(v1.handle.value - offset, v1.owner.handle.value)
+            # create a view
+            v2 = v1.view(offset)
+            self.assertEqual(v2.owner.handle.value, m.handle.value)
+            self.assertEqual(v2.owner.handle.value, m.handle.value)
+            self.assertEqual(v2.handle.value - offset * 2,
+                             v2.owner.handle.value)
+            self.assertEqual(m.refct, 3)
+            del v2
+            self.assertEqual(m.refct, 2)
+            del v1
+            self.assertEqual(m.refct, 1)
+
+        m = self.context.memalloc(1024)
+        check(m=m, offset=0)
+        check(m=m, offset=1)
+
 
 @skip_on_cudasim('CUDA Memory API unsupported in the simulator')
 class TestCudaMemoryFunctions(CUDATestCase):