1
# Owner(s): ["module: unknown"]
6
import torch.testing._internal.common_utils as common
7
from torch.testing._internal.common_cuda import (
12
from torch.testing._internal.common_utils import TEST_NUMPY
22
class TestNumbaIntegration(common.TestCase):
23
@unittest.skipIf(not TEST_NUMPY, "No numpy")
24
@unittest.skipIf(not TEST_CUDA, "No cuda")
25
def test_cuda_array_interface(self):
26
"""torch.Tensor exposes __cuda_array_interface__ for cuda tensors.
28
An object t is considered a cuda-tensor if:
29
hasattr(t, '__cuda_array_interface__')
31
A cuda-tensor provides a tensor description dict:
32
shape: (integer, ...) Tensor shape.
33
strides: (integer, ...) Tensor strides, in bytes.
34
typestr: (str) A numpy-style typestr.
35
data: (int, boolean) A (data_ptr, read-only) tuple.
36
version: (int) Version 0
39
https://numba.pydata.org/numba-doc/latest/cuda/cuda_array_interface.html
62
for tp, npt in zip(types, dtypes):
63
# CPU tensors do not implement the interface.
66
self.assertFalse(hasattr(cput, "__cuda_array_interface__"))
67
self.assertRaises(AttributeError, lambda: cput.__cuda_array_interface__)
69
# Sparse CPU/CUDA tensors do not implement the interface
70
if tp not in (torch.HalfTensor,):
71
indices_t = torch.empty(1, cput.size(0), dtype=torch.long).clamp_(min=0)
72
sparse_t = torch.sparse_coo_tensor(indices_t, cput)
74
self.assertFalse(hasattr(sparse_t, "__cuda_array_interface__"))
76
AttributeError, lambda: sparse_t.__cuda_array_interface__
79
sparse_cuda_t = torch.sparse_coo_tensor(indices_t, cput).cuda()
81
self.assertFalse(hasattr(sparse_cuda_t, "__cuda_array_interface__"))
83
AttributeError, lambda: sparse_cuda_t.__cuda_array_interface__
86
# CUDA tensors have the attribute and v2 interface
89
self.assertTrue(hasattr(cudat, "__cuda_array_interface__"))
91
ar_dict = cudat.__cuda_array_interface__
94
set(ar_dict.keys()), {"shape", "strides", "typestr", "data", "version"}
97
self.assertEqual(ar_dict["shape"], (10,))
98
self.assertIs(ar_dict["strides"], None)
99
# typestr from numpy, cuda-native little-endian
100
self.assertEqual(ar_dict["typestr"], numpy.dtype(npt).newbyteorder("<").str)
101
self.assertEqual(ar_dict["data"], (cudat.data_ptr(), False))
102
self.assertEqual(ar_dict["version"], 2)
104
@unittest.skipIf(not TEST_CUDA, "No cuda")
105
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
106
def test_array_adaptor(self):
107
"""Torch __cuda_array_adaptor__ exposes tensor data to numba.cuda."""
126
for dt in torch_dtypes:
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)
132
self.assertTrue(not numba.cuda.is_cuda_array(cput))
133
with self.assertRaises(TypeError):
134
numba.cuda.as_cuda_array(cput)
136
# Any cuda tensor is a cuda array.
137
cudat = cput.to(device="cuda")
138
self.assertTrue(numba.cuda.is_cuda_array(cudat))
140
numba_view = numba.cuda.as_cuda_array(cudat)
141
self.assertIsInstance(numba_view, numba.cuda.devicearray.DeviceNDArray)
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)
148
# Pass back to cuda from host for all equality checks below, needed for
149
# float16 comparisons, which aren't supported cpu-side.
151
# The data is identical in the view.
152
self.assertEqual(cudat, torch.tensor(numba_view.copy_to_host()).to("cuda"))
154
# Writes to the torch.Tensor are reflected in the numba array.
156
self.assertEqual(cudat, torch.tensor(numba_view.copy_to_host()).to("cuda"))
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)
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)
167
# As of numba 0.40.0 support for strided views is ...limited...
168
# Cannot verify correctness of strided view operations.
170
@unittest.skipIf(not TEST_CUDA, "No cuda")
171
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
172
def test_conversion_errors(self):
173
"""Numba properly detects array interface for tensor.Tensor variants."""
175
# CPU tensors are not cuda arrays.
176
cput = torch.arange(100)
178
self.assertFalse(numba.cuda.is_cuda_array(cput))
179
with self.assertRaises(TypeError):
180
numba.cuda.as_cuda_array(cput)
182
# Sparse tensors are not cuda arrays, regardless of device.
183
sparset = torch.sparse_coo_tensor(cput[None, :], cput)
185
self.assertFalse(numba.cuda.is_cuda_array(sparset))
186
with self.assertRaises(TypeError):
187
numba.cuda.as_cuda_array(sparset)
189
sparse_cuda_t = sparset.cuda()
191
self.assertFalse(numba.cuda.is_cuda_array(sparset))
192
with self.assertRaises(TypeError):
193
numba.cuda.as_cuda_array(sparset)
195
# Device-status overrides gradient status.
196
# CPU+gradient isn't a cuda array.
197
cpu_gradt = torch.zeros(100).requires_grad_(True)
199
self.assertFalse(numba.cuda.is_cuda_array(cpu_gradt))
200
with self.assertRaises(TypeError):
201
numba.cuda.as_cuda_array(cpu_gradt)
203
# CUDA+gradient raises a RuntimeError on check or conversion.
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()
209
# conversion raises RuntimeError
210
with self.assertRaises(RuntimeError):
211
numba.cuda.is_cuda_array(cuda_gradt)
212
with self.assertRaises(RuntimeError):
213
numba.cuda.as_cuda_array(cuda_gradt)
215
@unittest.skipIf(not TEST_CUDA, "No cuda")
216
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
217
@unittest.skipIf(not TEST_MULTIGPU, "No multigpu")
218
def test_active_device(self):
219
"""'as_cuda_array' tensor device must match active numba context."""
221
# Both torch/numba default to device 0 and can interop freely
222
cudat = torch.arange(10, device="cuda")
223
self.assertEqual(cudat.device.index, 0)
224
self.assertIsInstance(
225
numba.cuda.as_cuda_array(cudat), numba.cuda.devicearray.DeviceNDArray
228
# Tensors on non-default device raise api error if converted
229
cudat = torch.arange(10, device=torch.device("cuda", 1))
231
with self.assertRaises(numba.cuda.driver.CudaAPIError):
232
numba.cuda.as_cuda_array(cudat)
234
# but can be converted when switching to the device's context
235
with numba.cuda.devices.gpus[cudat.device.index]:
236
self.assertIsInstance(
237
numba.cuda.as_cuda_array(cudat), numba.cuda.devicearray.DeviceNDArray
241
"Test is temporary disabled, see https://github.com/pytorch/pytorch/issues/54418"
243
@unittest.skipIf(not TEST_NUMPY, "No numpy")
244
@unittest.skipIf(not TEST_CUDA, "No cuda")
245
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
246
def test_from_cuda_array_interface(self):
247
"""torch.as_tensor() and torch.tensor() supports the __cuda_array_interface__ protocol.
249
If an object exposes the __cuda_array_interface__, .as_tensor() and .tensor()
250
will use the exposed device memory.
253
https://numba.pydata.org/numba-doc/latest/cuda/cuda_array_interface.html
269
numpy.ones((), dtype=dtype),
270
numpy.arange(6).reshape(2, 3).astype(dtype),
273
.astype(dtype)[1:], # View offset should be ignored
276
.astype(dtype)[:, None], # change the strides but still contiguous
278
# Zero-copy when using `torch.as_tensor()`
279
for numpy_ary in numpy_arys:
280
numba_ary = numba.cuda.to_device(numpy_ary)
281
torch_ary = torch.as_tensor(numba_ary, device="cuda")
283
numba_ary.__cuda_array_interface__,
284
torch_ary.__cuda_array_interface__,
287
torch_ary.cpu().data.numpy(), numpy.asarray(numba_ary, dtype=dtype)
290
# Check that `torch_ary` and `numba_ary` points to the same device memory
293
torch_ary.cpu().data.numpy(), numpy.asarray(numba_ary, dtype=dtype)
296
# Implicit-copy because `torch_ary` is a CPU array
297
for numpy_ary in numpy_arys:
298
numba_ary = numba.cuda.to_device(numpy_ary)
299
torch_ary = torch.as_tensor(numba_ary, device="cpu")
301
torch_ary.data.numpy(), numpy.asarray(numba_ary, dtype=dtype)
304
# Check that `torch_ary` and `numba_ary` points to different memory
307
torch_ary.data.numpy(), numpy.asarray(numba_ary, dtype=dtype) + 42
310
# Explicit-copy when using `torch.tensor()`
311
for numpy_ary in numpy_arys:
312
numba_ary = numba.cuda.to_device(numpy_ary)
313
torch_ary = torch.tensor(numba_ary, device="cuda")
315
torch_ary.cpu().data.numpy(), numpy.asarray(numba_ary, dtype=dtype)
318
# Check that `torch_ary` and `numba_ary` points to different memory
321
torch_ary.cpu().data.numpy(),
322
numpy.asarray(numba_ary, dtype=dtype) + 42,
325
@unittest.skipIf(not TEST_NUMPY, "No numpy")
326
@unittest.skipIf(not TEST_CUDA, "No cuda")
327
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
328
def test_from_cuda_array_interface_inferred_strides(self):
329
"""torch.as_tensor(numba_ary) should have correct inferred (contiguous) strides"""
330
# This could, in theory, be combined with test_from_cuda_array_interface but that test
331
# is overly strict: it checks that the exported protocols are exactly the same, which
332
# cannot handle differing exported protocol versions.
343
numpy_ary = numpy.arange(6).reshape(2, 3).astype(dtype)
344
numba_ary = numba.cuda.to_device(numpy_ary)
345
self.assertTrue(numba_ary.is_c_contiguous())
346
torch_ary = torch.as_tensor(numba_ary, device="cuda")
347
self.assertTrue(torch_ary.is_contiguous())
350
"Test is temporary disabled, see https://github.com/pytorch/pytorch/issues/54418"
352
@unittest.skipIf(not TEST_NUMPY, "No numpy")
353
@unittest.skipIf(not TEST_CUDA, "No cuda")
354
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
355
def test_from_cuda_array_interface_lifetime(self):
356
"""torch.as_tensor(obj) tensor grabs a reference to obj so that the lifetime of obj exceeds the tensor"""
357
numba_ary = numba.cuda.to_device(numpy.arange(6))
358
torch_ary = torch.as_tensor(numba_ary, device="cuda")
360
torch_ary.__cuda_array_interface__, numba_ary.__cuda_array_interface__
364
torch_ary.cpu().data.numpy(), numpy.arange(6)
365
) # `torch_ary` is still alive
368
"Test is temporary disabled, see https://github.com/pytorch/pytorch/issues/54418"
370
@unittest.skipIf(not TEST_NUMPY, "No numpy")
371
@unittest.skipIf(not TEST_CUDA, "No cuda")
372
@unittest.skipIf(not TEST_NUMBA_CUDA, "No numba.cuda")
373
@unittest.skipIf(not TEST_MULTIGPU, "No multigpu")
374
def test_from_cuda_array_interface_active_device(self):
375
"""torch.as_tensor() tensor device must match active numba context."""
377
# Zero-copy: both torch/numba default to device 0 and can interop freely
378
numba_ary = numba.cuda.to_device(numpy.arange(6))
379
torch_ary = torch.as_tensor(numba_ary, device="cuda")
380
self.assertEqual(torch_ary.cpu().data.numpy(), numpy.asarray(numba_ary))
382
torch_ary.__cuda_array_interface__, numba_ary.__cuda_array_interface__
385
# Implicit-copy: when the Numba and Torch device differ
386
numba_ary = numba.cuda.to_device(numpy.arange(6))
387
torch_ary = torch.as_tensor(numba_ary, device=torch.device("cuda", 1))
388
self.assertEqual(torch_ary.get_device(), 1)
389
self.assertEqual(torch_ary.cpu().data.numpy(), numpy.asarray(numba_ary))
390
if1 = torch_ary.__cuda_array_interface__
391
if2 = numba_ary.__cuda_array_interface__
392
self.assertNotEqual(if1["data"], if2["data"])
395
self.assertEqual(if1, if2)
398
if __name__ == "__main__":