Caffe2 - Python API
A deep learning, cross platform ML framework
test_numba_integration.py
1 import unittest
2 import sys
3 
4 import common_utils as common
5 from common_utils import TEST_NUMBA, TEST_NUMPY, IS_WINDOWS
6 from common_cuda import TEST_NUMBA_CUDA, TEST_CUDA, TEST_MULTIGPU
7 
8 import torch
9 
10 if TEST_NUMPY:
11  import numpy
12 
13 if TEST_NUMBA:
14  import numba
15 
16 if TEST_NUMBA_CUDA:
17  import numba.cuda
18 
19 
20 class TestNumbaIntegration(common.TestCase):
21  @unittest.skipIf(not TEST_NUMPY, "No numpy")
22  @unittest.skipIf(not TEST_CUDA, "No cuda")
24  """torch.Tensor exposes __cuda_array_interface__ for cuda tensors.
25 
26  An object t is considered a cuda-tensor if:
27  hasattr(t, '__cuda_array_interface__')
28 
29  A cuda-tensor provides a tensor description dict:
30  shape: (integer, ...) Tensor shape.
31  strides: (integer, ...) Tensor strides, in bytes.
32  typestr: (str) A numpy-style typestr.
33  data: (int, boolean) A (data_ptr, read-only) tuple.
34  version: (int) Version 0
35 
36  See:
37  https://numba.pydata.org/numba-doc/latest/cuda/cuda_array_interface.html
38  """
39 
40  types = [
41  torch.DoubleTensor,
42  torch.FloatTensor,
43  torch.HalfTensor,
44  torch.LongTensor,
45  torch.IntTensor,
46  torch.ShortTensor,
47  torch.CharTensor,
48  torch.ByteTensor,
49  ]
50  dtypes = [
51  numpy.float64,
52  numpy.float32,
53  numpy.float16,
54  numpy.int64,
55  numpy.int32,
56  numpy.int16,
57  numpy.int8,
58  numpy.uint8,
59  ]
60  for tp, npt in zip(types, dtypes):
61 
62  # CPU tensors do not implement the interface.
63  cput = tp(10)
64 
65  self.assertFalse(hasattr(cput, "__cuda_array_interface__"))
66  self.assertRaises(AttributeError, lambda: cput.__cuda_array_interface__)
67 
68  # Sparse CPU/CUDA tensors do not implement the interface
69  if tp not in (torch.HalfTensor,):
70  indices_t = torch.empty(1, cput.size(0), dtype=torch.long).clamp_(min=0)
71  sparse_t = torch.sparse_coo_tensor(indices_t, cput)
72 
73  self.assertFalse(hasattr(sparse_t, "__cuda_array_interface__"))
74  self.assertRaises(
75  AttributeError, lambda: sparse_t.__cuda_array_interface__
76  )
77 
78  sparse_cuda_t = torch.sparse_coo_tensor(indices_t, cput).cuda()
79 
80  self.assertFalse(hasattr(sparse_cuda_t, "__cuda_array_interface__"))
81  self.assertRaises(
82  AttributeError, lambda: sparse_cuda_t.__cuda_array_interface__
83  )
84 
85  # CUDA tensors have the attribute and v0 interface
86  cudat = tp(10).cuda()
87 
88  self.assertTrue(hasattr(cudat, "__cuda_array_interface__"))
89 
90  ar_dict = cudat.__cuda_array_interface__
91 
92  self.assertEqual(
93  set(ar_dict.keys()), {"shape", "strides", "typestr", "data", "version"}
94  )
95 
96  self.assertEqual(ar_dict["shape"], (10,))
97  self.assertEqual(ar_dict["strides"], (cudat.storage().element_size(),))
98  # typestr from numpy, cuda-native little-endian
99  self.assertEqual(ar_dict["typestr"], numpy.dtype(npt).newbyteorder("<").str)
100  self.assertEqual(ar_dict["data"], (cudat.data_ptr(), False))
101  self.assertEqual(ar_dict["version"], 0)
102 
103  @unittest.skipIf(not TEST_CUDA, "No cuda")
104  @unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
106  """Torch __cuda_array_adaptor__ exposes tensor data to numba.cuda."""
107 
108  torch_dtypes = [
109  torch.float16,
110  torch.float32,
111  torch.float64,
112  torch.uint8,
113  torch.int8,
114  torch.int16,
115  torch.int32,
116  torch.int64,
117  ]
118 
119  for dt in torch_dtypes:
120  if dt == torch.int8 and not IS_WINDOWS:
121  # "CharTensor" numpy conversion not supported
122  with self.assertRaises(TypeError):
123  torch.arange(10).to(dt).numpy()
124 
125  continue
126 
127  # CPU tensors of all types do not register as cuda arrays,
128  # attempts to convert raise a type error.
129  cput = torch.arange(10).to(dt)
130  npt = cput.numpy()
131 
132  self.assertTrue(not numba.cuda.is_cuda_array(cput))
133  with self.assertRaises(TypeError):
134  numba.cuda.as_cuda_array(cput)
135 
136  # Any cuda tensor is a cuda array.
137  cudat = cput.to(device="cuda")
138  self.assertTrue(numba.cuda.is_cuda_array(cudat))
139 
140  numba_view = numba.cuda.as_cuda_array(cudat)
141  self.assertIsInstance(numba_view, numba.cuda.devicearray.DeviceNDArray)
142 
143  # The reported type of the cuda array matches the numpy type of the cpu tensor.
144  self.assertEqual(numba_view.dtype, npt.dtype)
145  self.assertEqual(numba_view.strides, npt.strides)
146  self.assertEqual(numba_view.shape, cudat.shape)
147 
148  # Pass back to cuda from host for all equality checks below, needed for
149  # float16 comparisons, which aren't supported cpu-side.
150 
151  # The data is identical in the view.
152  self.assertEqual(cudat, torch.tensor(numba_view.copy_to_host()).to("cuda"))
153 
154  # Writes to the torch.Tensor are reflected in the numba array.
155  cudat[:5] = 11
156  self.assertEqual(cudat, torch.tensor(numba_view.copy_to_host()).to("cuda"))
157 
158  # Strided tensors are supported.
159  strided_cudat = cudat[::2]
160  strided_npt = cput[::2].numpy()
161  strided_numba_view = numba.cuda.as_cuda_array(strided_cudat)
162 
163  self.assertEqual(strided_numba_view.dtype, strided_npt.dtype)
164  self.assertEqual(strided_numba_view.strides, strided_npt.strides)
165  self.assertEqual(strided_numba_view.shape, strided_cudat.shape)
166 
167  # As of numba 0.40.0 support for strided views is ...limited...
168  # Cannot verify correctness of strided view operations.
169 
170  @unittest.skipIf(not TEST_CUDA, "No cuda")
171  @unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
173  """Numba properly detects array interface for tensor.Tensor variants."""
174 
175  # CPU tensors are not cuda arrays.
176  cput = torch.arange(100)
177 
178  self.assertFalse(numba.cuda.is_cuda_array(cput))
179  with self.assertRaises(TypeError):
180  numba.cuda.as_cuda_array(cput)
181 
182  # Sparse tensors are not cuda arrays, regardless of device.
183  sparset = torch.sparse_coo_tensor(cput[None, :], cput)
184 
185  self.assertFalse(numba.cuda.is_cuda_array(sparset))
186  with self.assertRaises(TypeError):
187  numba.cuda.as_cuda_array(sparset)
188 
189  sparse_cuda_t = sparset.cuda()
190 
191  self.assertFalse(numba.cuda.is_cuda_array(sparset))
192  with self.assertRaises(TypeError):
193  numba.cuda.as_cuda_array(sparset)
194 
195  # Device-status overrides gradient status.
196  # CPU+gradient isn't a cuda array.
197  cpu_gradt = torch.zeros(100).requires_grad_(True)
198 
199  self.assertFalse(numba.cuda.is_cuda_array(cpu_gradt))
200  with self.assertRaises(TypeError):
201  numba.cuda.as_cuda_array(cpu_gradt)
202 
203  # CUDA+gradient raises a RuntimeError on check or conversion.
204  #
205  # Use of hasattr for interface detection causes interface change in
206  # python2; it swallows all exceptions not just AttributeError.
207  cuda_gradt = torch.zeros(100).requires_grad_(True).cuda()
208 
209  if sys.version_info.major > 2:
210  # 3+, conversion raises RuntimeError
211  with self.assertRaises(RuntimeError):
212  numba.cuda.is_cuda_array(cuda_gradt)
213  with self.assertRaises(RuntimeError):
214  numba.cuda.as_cuda_array(cuda_gradt)
215  else:
216  # 2, allow either RuntimeError on access or non-implementing
217  # behavior to future-proof against potential changes in numba.
218  try:
219  was_cuda_array = numba.cuda.is_cuda_array(cuda_gradt)
220  was_runtime_error = False
221  except RuntimeError:
222  was_cuda_array = False
223  was_runtime_error = True
224 
225  self.assertFalse(was_cuda_array)
226 
227  if not was_runtime_error:
228  with self.assertRaises(TypeError):
229  numba.cuda.as_cuda_array(cuda_gradt)
230  else:
231  with self.assertRaises(RuntimeError):
232  numba.cuda.as_cuda_array(cuda_gradt)
233 
234  @unittest.skipIf(not TEST_CUDA, "No cuda")
235  @unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
236  @unittest.skipIf(not TEST_MULTIGPU, "No multigpu")
238  """'as_cuda_array' tensor device must match active numba context."""
239 
240  # Both torch/numba default to device 0 and can interop freely
241  cudat = torch.arange(10, device="cuda")
242  self.assertEqual(cudat.device.index, 0)
243  self.assertIsInstance(
244  numba.cuda.as_cuda_array(cudat), numba.cuda.devicearray.DeviceNDArray
245  )
246 
247  # Tensors on non-default device raise api error if converted
248  cudat = torch.arange(10, device=torch.device("cuda", 1))
249 
250  with self.assertRaises(numba.cuda.driver.CudaAPIError):
251  numba.cuda.as_cuda_array(cudat)
252 
253  # but can be converted when switching to the device's context
254  with numba.cuda.devices.gpus[cudat.device.index]:
255  self.assertIsInstance(
256  numba.cuda.as_cuda_array(cudat), numba.cuda.devicearray.DeviceNDArray
257  )
258 
259 
260 if __name__ == "__main__":
261  common.run_tests()